Merge branch 'master' of https://github.com/koalaman/shellcheck into mrshu/pushd-popd-like-cd
Signed-off-by: mr.Shu <mr@shu.io>
This commit is contained in:
commit
79872f92f8
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# This script packages up Travis compiled binaries
|
||||||
|
set -ex
|
||||||
|
shopt -s nullglob
|
||||||
|
cd deploy
|
||||||
|
|
||||||
|
cp ../LICENSE LICENSE.txt
|
||||||
|
sed -e $'s/$/\r/' > README.txt << END
|
||||||
|
This is a precompiled ShellCheck binary.
|
||||||
|
http://www.shellcheck.net/
|
||||||
|
|
||||||
|
ShellCheck is a static analysis tool for shell scripts.
|
||||||
|
It's licensed under the GNU General Public License v3.0.
|
||||||
|
Information and source code is available on the website.
|
||||||
|
|
||||||
|
This binary was compiled on $(date -u).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
====== Latest commits ======
|
||||||
|
|
||||||
|
$(git log -n 3)
|
||||||
|
END
|
||||||
|
|
||||||
|
for file in ./*.exe
|
||||||
|
do
|
||||||
|
zip "${file%.*}.zip" README.txt LICENSE.txt "$file"
|
||||||
|
done
|
||||||
|
|
||||||
|
for file in ./*
|
||||||
|
do
|
||||||
|
sha512sum "$file" > "$file.sha512sum"
|
||||||
|
done
|
||||||
|
|
28
.travis.yml
28
.travis.yml
|
@ -11,11 +11,35 @@ before_install:
|
||||||
export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || ([ -n "$TRAVIS_TAG" ] && echo "$TRAVIS_TAG") || echo "$TRAVIS_BRANCH")
|
export TAG=$([ "$TRAVIS_BRANCH" == "master" ] && echo "latest" || ([ -n "$TRAVIS_TAG" ] && echo "$TRAVIS_TAG") || echo "$TRAVIS_BRANCH")
|
||||||
|
|
||||||
script:
|
script:
|
||||||
|
- mkdir deploy
|
||||||
|
# Windows .exe
|
||||||
|
- docker pull koalaman/winghc
|
||||||
|
- docker run -v "$PWD:/appdata" koalaman/winghc cuib
|
||||||
|
- cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$TAG.exe"
|
||||||
|
# Linux Docker
|
||||||
- docker build -t builder -f Dockerfile_builder .
|
- docker build -t builder -f Dockerfile_builder .
|
||||||
- docker run --rm -it -v $(pwd):/mnt builder
|
- docker run --rm -it -v "$(pwd):/mnt" builder
|
||||||
- docker build -t $DOCKER_REPO:$TAG .
|
- docker build -t "$DOCKER_REPO:$TAG" .
|
||||||
|
|
||||||
after_success:
|
after_success:
|
||||||
|
- ./.prepare_deploy
|
||||||
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
- docker login -e="$DOCKER_EMAIL" -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
|
||||||
- |-
|
- |-
|
||||||
([ "$TRAVIS_BRANCH" == "master" ] || [ -n "$TRAVIS_TAG" ]) && docker push "$DOCKER_REPO:$TAG"
|
([ "$TRAVIS_BRANCH" == "master" ] || [ -n "$TRAVIS_TAG" ]) && docker push "$DOCKER_REPO:$TAG"
|
||||||
|
|
||||||
|
after_failure:
|
||||||
|
- pwd
|
||||||
|
- df -h
|
||||||
|
- find . -name '*.log' -type f -exec grep "" /dev/null {} +
|
||||||
|
- find .
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
provider: gcs
|
||||||
|
skip_cleanup: true
|
||||||
|
access_key_id: GOOG7MDN7WEH6IIGBDCA
|
||||||
|
secret_access_key:
|
||||||
|
secure: Bcx2cT0/E2ikj7sdamVq52xlLZF9dz9ojGPtoKfPyQhkkZa+McVI4xgUSuyyoSxyKj77sofx2y8m6PJYYumT4g5hREV1tfeUkl0J2DQFMbGDYEt7kxVkXCxojNvhHwTzLFv0ezstrxWWxQm81BfQQ4U9lggRXtndAP4czZnOeHPINPSiue1QNwRAEw05r5UoIUJXy/5xyUrjIxn381pAs+gJqP2COeN9kTKYH53nS/AAws29RprfZFnPlo7xxWmcjRcdS5KPdGXI/c6tQp5zl2iTh510VC1PN2w1Wvnn/oNWhiNdqPyVDsojIX5+sS3nejzJA+KFMxXSBlyXIY3wPpS/MdscU79X6Q5f9ivsFfsm7gNBmxHUPNn0HAvU4ROT/CCE9j6jSbs5PC7QBo3CK4++jxAwE/pd9HUc2rs3k0ofx3rgveJ7txpy5yPKfwIIBi98kVKlC4w7dLvNTOfjW1Imt2yH87XTfsE0UIG9st1WII6s4l/WgBx2GuwKdt6+3QUYiAlCFckkxWi+fAvpHZUEL43Qxub5fN+ZV7Zib1n7opchH4QKGBb6/y0WaDCmtCfu0lppoe/TH6saOTjDFj67NJSElK6ZDxGZ3uw4R+ret2gm6WRKT2Oeub8J33VzSa7VkmFpMPrAAfPa9N1Z4ewBLoTmvxSg2A0dDrCdJio=
|
||||||
|
bucket: shellcheck
|
||||||
|
local-dir: deploy
|
||||||
|
on:
|
||||||
|
repo: koalaman/shellcheck
|
||||||
|
|
18
README.md
18
README.md
|
@ -25,7 +25,7 @@ There are a variety of ways to use ShellCheck!
|
||||||
#### On the web
|
#### On the web
|
||||||
Paste a shell script on http://www.shellcheck.net for instant feedback.
|
Paste a shell script on http://www.shellcheck.net for instant feedback.
|
||||||
|
|
||||||
[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the simplest way to give ShellCheck a go. Tell your friends!
|
[ShellCheck.net](http://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
|
||||||
|
|
||||||
|
|
||||||
#### From your terminal
|
#### From your terminal
|
||||||
|
@ -126,13 +126,9 @@ or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
|
||||||
From Docker Hub:
|
From Docker Hub:
|
||||||
|
|
||||||
docker pull koalaman/shellcheck
|
docker pull koalaman/shellcheck
|
||||||
|
docker run -v "$PWD:/mnt" koalaman/shellcheck myscript
|
||||||
|
|
||||||
Using the Docker image can be done like so:
|
For Windows, you can download [precompiled Windows executables](https://storage.googleapis.com/shellcheck/shellcheck-latest.zip).
|
||||||
|
|
||||||
docker run -v $(pwd):/scripts koalaman/shellcheck /scripts/myscript.sh
|
|
||||||
|
|
||||||
Here the local directory ( $(pwd) ) is mounted into the containers directory "/scripts". The script "myscript.sh" is checked.
|
|
||||||
|
|
||||||
## Compiling from source
|
## Compiling from source
|
||||||
|
|
||||||
This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
|
This section describes how to build ShellCheck from a source directory. ShellCheck is written in Haskell and requires 2GB of RAM to compile.
|
||||||
|
@ -154,6 +150,10 @@ Verify that `cabal` is installed and update its dependency list with
|
||||||
|
|
||||||
$ cabal install
|
$ cabal install
|
||||||
|
|
||||||
|
Or if you intend to run the tests:
|
||||||
|
|
||||||
|
$ cabal install --enable-tests
|
||||||
|
|
||||||
This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
|
This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
|
||||||
|
|
||||||
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
|
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
|
||||||
|
@ -349,7 +349,9 @@ https://github.com/koalaman/shellcheck/issues
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Please submit patches to code or documentation as GitHub pull requests!
|
Please submit patches to code or documentation as GitHub pull requests! Check
|
||||||
|
out the [DevGuide](https://github.com/koalaman/shellcheck/wiki/DevGuide) on the
|
||||||
|
ShellCheck Wiki.
|
||||||
|
|
||||||
Contributions must be licensed under the GNU GPLv3.
|
Contributions must be licensed under the GNU GPLv3.
|
||||||
The contributor retains the copyright.
|
The contributor retains the copyright.
|
||||||
|
|
|
@ -143,7 +143,6 @@ nodeChecks = [
|
||||||
,checkWrongArithmeticAssignment
|
,checkWrongArithmeticAssignment
|
||||||
,checkConditionalAndOrs
|
,checkConditionalAndOrs
|
||||||
,checkFunctionDeclarations
|
,checkFunctionDeclarations
|
||||||
,checkCatastrophicRm
|
|
||||||
,checkStderrPipe
|
,checkStderrPipe
|
||||||
,checkOverridingPath
|
,checkOverridingPath
|
||||||
,checkArrayAsString
|
,checkArrayAsString
|
||||||
|
@ -661,15 +660,12 @@ prop_checkUnquotedDollarAt6 = verifyNot checkUnquotedDollarAt "a=$@"
|
||||||
prop_checkUnquotedDollarAt7 = verify checkUnquotedDollarAt "for f in ${var[@]}; do true; done"
|
prop_checkUnquotedDollarAt7 = verify checkUnquotedDollarAt "for f in ${var[@]}; do true; done"
|
||||||
prop_checkUnquotedDollarAt8 = verifyNot checkUnquotedDollarAt "echo \"${args[@]:+${args[@]}}\""
|
prop_checkUnquotedDollarAt8 = verifyNot checkUnquotedDollarAt "echo \"${args[@]:+${args[@]}}\""
|
||||||
prop_checkUnquotedDollarAt9 = verifyNot checkUnquotedDollarAt "echo ${args[@]:+\"${args[@]}\"}"
|
prop_checkUnquotedDollarAt9 = verifyNot checkUnquotedDollarAt "echo ${args[@]:+\"${args[@]}\"}"
|
||||||
|
prop_checkUnquotedDollarAt10 = verifyNot checkUnquotedDollarAt "echo ${@+\"$@\"}"
|
||||||
checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree (parentMap p) word =
|
checkUnquotedDollarAt p word@(T_NormalWord _ parts) | not $ isStrictlyQuoteFree (parentMap p) word =
|
||||||
forM_ (take 1 $ filter isArrayExpansion parts) $ \x ->
|
forM_ (take 1 $ filter isArrayExpansion parts) $ \x ->
|
||||||
unless (isAlternative x) $
|
unless (isQuotedAlternativeReference x) $
|
||||||
err (getId x) 2068
|
err (getId x) 2068
|
||||||
"Double quote array expansions to avoid re-splitting elements."
|
"Double quote array expansions to avoid re-splitting elements."
|
||||||
where
|
|
||||||
-- Fixme: should detect whether the alternative is quoted
|
|
||||||
isAlternative b@(T_DollarBraced _ t) = ":+" `isInfixOf` bracedString b
|
|
||||||
isAlternative _ = False
|
|
||||||
checkUnquotedDollarAt _ _ = return ()
|
checkUnquotedDollarAt _ _ = return ()
|
||||||
|
|
||||||
prop_checkConcatenatedDollarAt1 = verify checkConcatenatedDollarAt "echo \"foo$@\""
|
prop_checkConcatenatedDollarAt1 = verify checkConcatenatedDollarAt "echo \"foo$@\""
|
||||||
|
@ -1375,10 +1371,11 @@ prop_checkInexplicablyUnquoted4 = verify checkInexplicablyUnquoted "echo \"VALUE
|
||||||
prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\""
|
prop_checkInexplicablyUnquoted5 = verifyNot checkInexplicablyUnquoted "\"$dir\"/\"$file\""
|
||||||
prop_checkInexplicablyUnquoted6 = verifyNot checkInexplicablyUnquoted "\"$dir\"some_stuff\"$file\""
|
prop_checkInexplicablyUnquoted6 = verifyNot checkInexplicablyUnquoted "\"$dir\"some_stuff\"$file\""
|
||||||
prop_checkInexplicablyUnquoted7 = verifyNot checkInexplicablyUnquoted "${dir/\"foo\"/\"bar\"}"
|
prop_checkInexplicablyUnquoted7 = verifyNot checkInexplicablyUnquoted "${dir/\"foo\"/\"bar\"}"
|
||||||
|
prop_checkInexplicablyUnquoted8 = verifyNot checkInexplicablyUnquoted " 'foo'\\\n 'bar'"
|
||||||
checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens)
|
checkInexplicablyUnquoted _ (T_NormalWord id tokens) = mapM_ check (tails tokens)
|
||||||
where
|
where
|
||||||
check (T_SingleQuoted _ _:T_Literal id str:_)
|
check (T_SingleQuoted _ _:T_Literal id str:_)
|
||||||
| all isAlphaNum str =
|
| not (null str) && all isAlphaNum str =
|
||||||
info id 2026 "This word is outside of quotes. Did you intend to 'nest '\"'single quotes'\"' instead'? "
|
info id 2026 "This word is outside of quotes. Did you intend to 'nest '\"'single quotes'\"' instead'? "
|
||||||
|
|
||||||
check (T_DoubleQuoted _ a:trapped:T_DoubleQuoted _ b:_) =
|
check (T_DoubleQuoted _ a:trapped:T_DoubleQuoted _ b:_) =
|
||||||
|
@ -1527,6 +1524,7 @@ prop_subshellAssignmentCheck15 = verifyNotTree subshellAssignmentCheck "#!/bin/k
|
||||||
prop_subshellAssignmentCheck16 = verifyNotTree subshellAssignmentCheck "(set -e); echo $@"
|
prop_subshellAssignmentCheck16 = verifyNotTree subshellAssignmentCheck "(set -e); echo $@"
|
||||||
prop_subshellAssignmentCheck17 = verifyNotTree subshellAssignmentCheck "foo=${ { bar=$(baz); } 2>&1; }; echo $foo $bar"
|
prop_subshellAssignmentCheck17 = verifyNotTree subshellAssignmentCheck "foo=${ { bar=$(baz); } 2>&1; }; echo $foo $bar"
|
||||||
prop_subshellAssignmentCheck18 = verifyTree subshellAssignmentCheck "( exec {n}>&2; ); echo $n"
|
prop_subshellAssignmentCheck18 = verifyTree subshellAssignmentCheck "( exec {n}>&2; ); echo $n"
|
||||||
|
prop_subshellAssignmentCheck19 = verifyNotTree subshellAssignmentCheck "#!/bin/bash\nshopt -s lastpipe; echo a | read -r b; echo \"$b\""
|
||||||
subshellAssignmentCheck params t =
|
subshellAssignmentCheck params t =
|
||||||
let flow = variableFlow params
|
let flow = variableFlow params
|
||||||
check = findSubshelled flow [("oops",[])] Map.empty
|
check = findSubshelled flow [("oops",[])] Map.empty
|
||||||
|
@ -1611,6 +1609,7 @@ prop_checkSpacefulness31= verifyNotTree checkSpacefulness "echo \"`echo \\\"$1\\
|
||||||
prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
|
prop_checkSpacefulness32= verifyNotTree checkSpacefulness "var=$1; [ -v var ]"
|
||||||
prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"
|
prop_checkSpacefulness33= verifyTree checkSpacefulness "for file; do echo $file; done"
|
||||||
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
|
prop_checkSpacefulness34= verifyTree checkSpacefulness "declare foo$n=$1"
|
||||||
|
prop_checkSpacefulness35= verifyNotTree checkSpacefulness "echo ${1+\"$1\"}"
|
||||||
|
|
||||||
checkSpacefulness params t =
|
checkSpacefulness params t =
|
||||||
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
|
doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
|
||||||
|
@ -2110,7 +2109,7 @@ checkLoopKeywordScope params t |
|
||||||
where
|
where
|
||||||
name = getCommandName t
|
name = getCommandName t
|
||||||
path = let p = getPath (parentMap params) t in filter relevant p
|
path = let p = getPath (parentMap params) t in filter relevant p
|
||||||
subshellType t = case leadType (shellType params) (parentMap params) t of
|
subshellType t = case leadType params t of
|
||||||
NoneScope -> Nothing
|
NoneScope -> Nothing
|
||||||
SubshellScope str -> return str
|
SubshellScope str -> return str
|
||||||
relevant t = isLoop t || isFunction t || isJust (subshellType t)
|
relevant t = isLoop t || isFunction t || isJust (subshellType t)
|
||||||
|
@ -2139,72 +2138,6 @@ checkFunctionDeclarations params
|
||||||
checkFunctionDeclarations _ _ = return ()
|
checkFunctionDeclarations _ _ = return ()
|
||||||
|
|
||||||
|
|
||||||
prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2"
|
|
||||||
prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo"
|
|
||||||
prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"
|
|
||||||
prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"
|
|
||||||
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
|
|
||||||
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
|
|
||||||
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
|
|
||||||
prop_checkCatastrophicRm9 = verifyNot checkCatastrophicRm "rm -rf -- /home"
|
|
||||||
prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
|
|
||||||
prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
|
|
||||||
prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
|
|
||||||
prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
|
|
||||||
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
|
|
||||||
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
|
|
||||||
checkCatastrophicRm params t@(T_SimpleCommand id _ tokens) | t `isCommand` "rm" =
|
|
||||||
when (any isRecursiveFlag simpleArgs) $
|
|
||||||
mapM_ (mapM_ checkWord . braceExpand) tokens
|
|
||||||
where
|
|
||||||
simpleArgs = oversimplify t
|
|
||||||
|
|
||||||
checkWord token =
|
|
||||||
case getLiteralString token of
|
|
||||||
Just str ->
|
|
||||||
when (notElem "--" simpleArgs && (fixPath str `elem` importantPaths)) $
|
|
||||||
warn (getId token) 2114 "Warning: deletes a system directory. Use 'rm --' to disable this message."
|
|
||||||
Nothing ->
|
|
||||||
checkWord' token
|
|
||||||
|
|
||||||
checkWord' token = fromMaybe (return ()) $ do
|
|
||||||
filename <- getPotentialPath token
|
|
||||||
let path = fixPath filename
|
|
||||||
return . when (path `elem` importantPaths) $
|
|
||||||
warn (getId token) 2115 $ "Use \"${var:?}\" to ensure this never expands to " ++ path ++ " ."
|
|
||||||
|
|
||||||
fixPath filename =
|
|
||||||
let normalized = skipRepeating '/' . skipRepeating '*' $ filename in
|
|
||||||
if normalized == "/" then normalized else stripTrailing '/' normalized
|
|
||||||
|
|
||||||
getPotentialPath = getLiteralStringExt f
|
|
||||||
where
|
|
||||||
f (T_Glob _ str) = return str
|
|
||||||
f (T_DollarBraced _ word) =
|
|
||||||
let var = onlyLiteralString word in
|
|
||||||
if any (`isInfixOf` var) [":?", ":-", ":="]
|
|
||||||
then Nothing
|
|
||||||
else return ""
|
|
||||||
f _ = return ""
|
|
||||||
|
|
||||||
isRecursiveFlag "--recursive" = True
|
|
||||||
isRecursiveFlag ('-':'-':_) = False
|
|
||||||
isRecursiveFlag ('-':str) = 'r' `elem` str || 'R' `elem` str
|
|
||||||
isRecursiveFlag _ = False
|
|
||||||
|
|
||||||
stripTrailing c = reverse . dropWhile (== c) . reverse
|
|
||||||
skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest)
|
|
||||||
skipRepeating c (a:r) = a:skipRepeating c r
|
|
||||||
skipRepeating _ [] = []
|
|
||||||
|
|
||||||
paths = [
|
|
||||||
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
|
|
||||||
"/var", "/lib"
|
|
||||||
]
|
|
||||||
importantPaths = filter (not . null) $
|
|
||||||
["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths)
|
|
||||||
checkCatastrophicRm _ _ = return ()
|
|
||||||
|
|
||||||
|
|
||||||
prop_checkStderrPipe1 = verify checkStderrPipe "#!/bin/ksh\nfoo |& bar"
|
prop_checkStderrPipe1 = verify checkStderrPipe "#!/bin/ksh\nfoo |& bar"
|
||||||
prop_checkStderrPipe2 = verifyNot checkStderrPipe "#!/bin/bash\nfoo |& bar"
|
prop_checkStderrPipe2 = verifyNot checkStderrPipe "#!/bin/bash\nfoo |& bar"
|
||||||
|
@ -2238,7 +2171,7 @@ checkUnpassedInFunctions params root =
|
||||||
functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root
|
functions = execWriter $ doAnalysis (tell . maybeToList . findFunction) root
|
||||||
|
|
||||||
findFunction t@(T_Function id _ _ name body) =
|
findFunction t@(T_Function id _ _ name body) =
|
||||||
let flow = getVariableFlow (shellType params) (parentMap params) body
|
let flow = getVariableFlow params body
|
||||||
in
|
in
|
||||||
if any (isPositionalReference t) flow && not (any isPositionalAssignment flow)
|
if any (isPositionalReference t) flow && not (any isPositionalAssignment flow)
|
||||||
then return t
|
then return t
|
||||||
|
@ -2541,7 +2474,9 @@ prop_checkUncheckedCd6 = verifyNotTree checkUncheckedCd "cd .."
|
||||||
prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCd "#!/bin/bash -e\ncd foo\nrm bar"
|
prop_checkUncheckedCd7 = verifyNotTree checkUncheckedCd "#!/bin/bash -e\ncd foo\nrm bar"
|
||||||
prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCd "set -o errexit; cd foo; rm bar"
|
prop_checkUncheckedCd8 = verifyNotTree checkUncheckedCd "set -o errexit; cd foo; rm bar"
|
||||||
checkUncheckedCd params root =
|
checkUncheckedCd params root =
|
||||||
if hasSetE root then [] else execWriter $ doAnalysis checkElement root
|
if hasSetE params
|
||||||
|
then []
|
||||||
|
else execWriter $ doAnalysis checkElement root
|
||||||
where
|
where
|
||||||
checkElement t@T_SimpleCommand {} =
|
checkElement t@T_SimpleCommand {} =
|
||||||
when(t `isUnqualifiedCommand` "cd"
|
when(t `isUnqualifiedCommand` "cd"
|
||||||
|
|
|
@ -72,11 +72,13 @@ composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
|
||||||
composeAnalyzers f g x = f x >> g x
|
composeAnalyzers f g x = f x >> g x
|
||||||
|
|
||||||
data Parameters = Parameters {
|
data Parameters = Parameters {
|
||||||
variableFlow :: [StackData],
|
hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default.
|
||||||
parentMap :: Map.Map Id Token,
|
hasSetE :: Bool, -- Whether this script has 'set -e' anywhere.
|
||||||
shellType :: Shell,
|
variableFlow :: [StackData], -- A linear (bad) analysis of data flow
|
||||||
shellTypeSpecified :: Bool,
|
parentMap :: Map.Map Id Token, -- A map from Id to parent Token
|
||||||
rootNode :: Token
|
shellType :: Shell, -- The shell type, such as Bash or Ksh
|
||||||
|
shellTypeSpecified :: Bool, -- True if shell type was forced via flags
|
||||||
|
rootNode :: Token -- The root node of the AST
|
||||||
}
|
}
|
||||||
|
|
||||||
-- TODO: Cache results of common AST ops here
|
-- TODO: Cache results of common AST ops here
|
||||||
|
@ -142,13 +144,48 @@ makeParameters spec =
|
||||||
let params = Parameters {
|
let params = Parameters {
|
||||||
rootNode = root,
|
rootNode = root,
|
||||||
shellType = fromMaybe (determineShell root) $ asShellType spec,
|
shellType = fromMaybe (determineShell root) $ asShellType spec,
|
||||||
|
hasSetE = containsSetE root,
|
||||||
|
hasLastpipe =
|
||||||
|
case shellType params of
|
||||||
|
Bash -> containsLastpipe root
|
||||||
|
Dash -> False
|
||||||
|
Sh -> False
|
||||||
|
Ksh -> True,
|
||||||
|
|
||||||
shellTypeSpecified = isJust $ asShellType spec,
|
shellTypeSpecified = isJust $ asShellType spec,
|
||||||
parentMap = getParentTree root,
|
parentMap = getParentTree root,
|
||||||
variableFlow =
|
variableFlow = getVariableFlow params root
|
||||||
getVariableFlow (shellType params) (parentMap params) root
|
|
||||||
} in params
|
} in params
|
||||||
where root = asScript spec
|
where root = asScript spec
|
||||||
|
|
||||||
|
|
||||||
|
-- Does this script mention 'set -e' anywhere?
|
||||||
|
-- Used as a hack to disable certain warnings.
|
||||||
|
containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
|
||||||
|
where
|
||||||
|
isSetE t =
|
||||||
|
case t of
|
||||||
|
T_Script _ str _ -> str `matches` re
|
||||||
|
T_SimpleCommand {} ->
|
||||||
|
t `isUnqualifiedCommand` "set" &&
|
||||||
|
("errexit" `elem` oversimplify t ||
|
||||||
|
"e" `elem` map snd (getAllFlags t))
|
||||||
|
_ -> False
|
||||||
|
re = mkRegex "[[:space:]]-[^-]*e"
|
||||||
|
|
||||||
|
-- Does this script mention 'shopt -s lastpipe' anywhere?
|
||||||
|
-- Also used as a hack.
|
||||||
|
containsLastpipe root =
|
||||||
|
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
|
||||||
|
where
|
||||||
|
isShoptLastPipe t =
|
||||||
|
case t of
|
||||||
|
T_SimpleCommand {} ->
|
||||||
|
t `isUnqualifiedCommand` "shopt" &&
|
||||||
|
("lastpipe" `elem` oversimplify t)
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
|
||||||
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh
|
prop_determineShell0 = determineShell (fromJust $ pScript "#!/bin/sh") == Sh
|
||||||
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
|
prop_determineShell1 = determineShell (fromJust $ pScript "#!/usr/bin/env ksh") == Ksh
|
||||||
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash
|
prop_determineShell2 = determineShell (fromJust $ pScript "") == Bash
|
||||||
|
@ -184,8 +221,10 @@ executableFromShebang = shellFor
|
||||||
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
|
shellFor s = reverse . takeWhile (/= '/') . reverse $ s
|
||||||
|
|
||||||
|
|
||||||
--- Context seeking
|
|
||||||
|
|
||||||
|
-- Given a root node, make a map from Id to parent Token.
|
||||||
|
-- This is used to populate parentMap in Parameters
|
||||||
|
getParentTree :: Token -> Map.Map Id Token
|
||||||
getParentTree t =
|
getParentTree t =
|
||||||
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
|
snd . snd $ runState (doStackAnalysis pre post t) ([], Map.empty)
|
||||||
where
|
where
|
||||||
|
@ -195,18 +234,24 @@ getParentTree t =
|
||||||
case rest of [] -> put (rest, map)
|
case rest of [] -> put (rest, map)
|
||||||
(x:_) -> put (rest, Map.insert (getId t) x map)
|
(x:_) -> put (rest, Map.insert (getId t) x map)
|
||||||
|
|
||||||
|
-- Given a root node, make a map from Id to Token
|
||||||
|
getTokenMap :: Token -> Map.Map Id Token
|
||||||
getTokenMap t =
|
getTokenMap t =
|
||||||
execState (doAnalysis f t) Map.empty
|
execState (doAnalysis f t) Map.empty
|
||||||
where
|
where
|
||||||
f t = modify (Map.insert (getId t) t)
|
f t = modify (Map.insert (getId t) t)
|
||||||
|
|
||||||
|
|
||||||
-- Is this node self quoting for a regular element?
|
-- Is this token in a quoting free context? (i.e. would variable expansion split)
|
||||||
isQuoteFree = isQuoteFreeNode False
|
-- True: Assignments, [[ .. ]], here docs, already in double quotes
|
||||||
|
-- False: Regular words
|
||||||
-- Is this node striclty self quoting, for array expansions
|
|
||||||
isStrictlyQuoteFree = isQuoteFreeNode True
|
isStrictlyQuoteFree = isQuoteFreeNode True
|
||||||
|
|
||||||
|
-- Like above, but also allow some cases where splitting may be desired.
|
||||||
|
-- True: Like above + for loops
|
||||||
|
-- False: Like above
|
||||||
|
isQuoteFree = isQuoteFreeNode False
|
||||||
|
|
||||||
|
|
||||||
isQuoteFreeNode strict tree t =
|
isQuoteFreeNode strict tree t =
|
||||||
(isQuoteFreeElement t == Just True) ||
|
(isQuoteFreeElement t == Just True) ||
|
||||||
|
@ -239,6 +284,9 @@ isQuoteFreeNode strict tree t =
|
||||||
T_SelectIn {} -> return (not strict)
|
T_SelectIn {} -> return (not strict)
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
|
-- Check if a token is a parameter to a certain command by name:
|
||||||
|
-- Example: isParamTo (parentMap params) "sed" t
|
||||||
|
isParamTo :: Map.Map Id Token -> String -> Token -> Bool
|
||||||
isParamTo tree cmd =
|
isParamTo tree cmd =
|
||||||
go
|
go
|
||||||
where
|
where
|
||||||
|
@ -254,16 +302,23 @@ isParamTo tree cmd =
|
||||||
T_Redirecting {} -> isCommand t cmd
|
T_Redirecting {} -> isCommand t cmd
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
|
-- Get the parent command (T_Redirecting) of a Token, if any.
|
||||||
|
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
|
||||||
getClosestCommand tree t =
|
getClosestCommand tree t =
|
||||||
msum . map getCommand $ getPath tree t
|
findFirst findCommand $ getPath tree t
|
||||||
where
|
where
|
||||||
getCommand t@T_Redirecting {} = return t
|
findCommand t =
|
||||||
getCommand _ = Nothing
|
case t of
|
||||||
|
T_Redirecting {} -> return True
|
||||||
|
T_Script {} -> return False
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
-- Like above, if koala_man knew Haskell when starting this project.
|
||||||
getClosestCommandM t = do
|
getClosestCommandM t = do
|
||||||
tree <- asks parentMap
|
tree <- asks parentMap
|
||||||
return $ getClosestCommand tree t
|
return $ getClosestCommand tree t
|
||||||
|
|
||||||
|
-- Is the token used as a command name (the first word in a T_SimpleCommand)?
|
||||||
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||||
where
|
where
|
||||||
go currentId (T_NormalWord id [word]:rest)
|
go currentId (T_NormalWord id [word]:rest)
|
||||||
|
@ -274,7 +329,7 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||||
| currentId == getId word = True
|
| currentId == getId word = True
|
||||||
go _ _ = False
|
go _ _ = False
|
||||||
|
|
||||||
-- A list of the element and all its parents
|
-- A list of the element and all its parents up to the root node.
|
||||||
getPath tree t = t :
|
getPath tree t = t :
|
||||||
case Map.lookup (getId t) tree of
|
case Map.lookup (getId t) tree of
|
||||||
Nothing -> []
|
Nothing -> []
|
||||||
|
@ -295,6 +350,18 @@ pathTo t = do
|
||||||
parents <- reader parentMap
|
parents <- reader parentMap
|
||||||
return $ getPath parents t
|
return $ getPath parents t
|
||||||
|
|
||||||
|
-- Find the first match in a list where the predicate is Just True.
|
||||||
|
-- Stops if it's Just False and ignores Nothing.
|
||||||
|
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
|
||||||
|
findFirst p l =
|
||||||
|
case l of
|
||||||
|
[] -> Nothing
|
||||||
|
(x:xs) ->
|
||||||
|
case p x of
|
||||||
|
Just True -> return x
|
||||||
|
Just False -> Nothing
|
||||||
|
Nothing -> findFirst p xs
|
||||||
|
|
||||||
-- Check whether a word is entirely output from a single command
|
-- Check whether a word is entirely output from a single command
|
||||||
tokenIsJustCommandOutput t = case t of
|
tokenIsJustCommandOutput t = case t of
|
||||||
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
|
T_NormalWord id [T_DollarExpansion _ cmds] -> check cmds
|
||||||
|
@ -307,18 +374,18 @@ tokenIsJustCommandOutput t = case t of
|
||||||
check _ = False
|
check _ = False
|
||||||
|
|
||||||
-- TODO: Replace this with a proper Control Flow Graph
|
-- TODO: Replace this with a proper Control Flow Graph
|
||||||
getVariableFlow shell parents t =
|
getVariableFlow params t =
|
||||||
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
|
let (_, stack) = runState (doStackAnalysis startScope endScope t) []
|
||||||
in reverse stack
|
in reverse stack
|
||||||
where
|
where
|
||||||
startScope t =
|
startScope t =
|
||||||
let scopeType = leadType shell parents t
|
let scopeType = leadType params t
|
||||||
in do
|
in do
|
||||||
when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
|
when (scopeType /= NoneScope) $ modify (StackScope scopeType:)
|
||||||
when (assignFirst t) $ setWritten t
|
when (assignFirst t) $ setWritten t
|
||||||
|
|
||||||
endScope t =
|
endScope t =
|
||||||
let scopeType = leadType shell parents t
|
let scopeType = leadType params t
|
||||||
in do
|
in do
|
||||||
setRead t
|
setRead t
|
||||||
unless (assignFirst t) $ setWritten t
|
unless (assignFirst t) $ setWritten t
|
||||||
|
@ -329,7 +396,7 @@ getVariableFlow shell parents t =
|
||||||
assignFirst _ = False
|
assignFirst _ = False
|
||||||
|
|
||||||
setRead t =
|
setRead t =
|
||||||
let read = getReferencedVariables parents t
|
let read = getReferencedVariables (parentMap params) t
|
||||||
in mapM_ (\v -> modify (Reference v:)) read
|
in mapM_ (\v -> modify (Reference v:)) read
|
||||||
|
|
||||||
setWritten t =
|
setWritten t =
|
||||||
|
@ -337,7 +404,7 @@ getVariableFlow shell parents t =
|
||||||
in mapM_ (\v -> modify (Assignment v:)) written
|
in mapM_ (\v -> modify (Assignment v:)) written
|
||||||
|
|
||||||
|
|
||||||
leadType shell parents t =
|
leadType params t =
|
||||||
case t of
|
case t of
|
||||||
T_DollarExpansion _ _ -> SubshellScope "$(..) expansion"
|
T_DollarExpansion _ _ -> SubshellScope "$(..) expansion"
|
||||||
T_Backticked _ _ -> SubshellScope "`..` expansion"
|
T_Backticked _ _ -> SubshellScope "`..` expansion"
|
||||||
|
@ -351,7 +418,7 @@ leadType shell parents t =
|
||||||
_ -> NoneScope
|
_ -> NoneScope
|
||||||
where
|
where
|
||||||
parentPipeline = do
|
parentPipeline = do
|
||||||
parent <- Map.lookup (getId t) parents
|
parent <- Map.lookup (getId t) (parentMap params)
|
||||||
case parent of
|
case parent of
|
||||||
T_Pipeline {} -> return parent
|
T_Pipeline {} -> return parent
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
@ -360,17 +427,10 @@ leadType shell parents t =
|
||||||
(T_Pipeline _ _ list) <- parentPipeline
|
(T_Pipeline _ _ list) <- parentPipeline
|
||||||
if length list <= 1
|
if length list <= 1
|
||||||
then return False
|
then return False
|
||||||
else if lastCreatesSubshell
|
else if not $ hasLastpipe params
|
||||||
then return True
|
then return True
|
||||||
else return . not $ (getId . head $ reverse list) == getId t
|
else return . not $ (getId . head $ reverse list) == getId t
|
||||||
|
|
||||||
lastCreatesSubshell =
|
|
||||||
case shell of
|
|
||||||
Bash -> True
|
|
||||||
Dash -> True
|
|
||||||
Sh -> True
|
|
||||||
Ksh -> False
|
|
||||||
|
|
||||||
getModifiedVariables t =
|
getModifiedVariables t =
|
||||||
case t of
|
case t of
|
||||||
T_SimpleCommand _ vars [] ->
|
T_SimpleCommand _ vars [] ->
|
||||||
|
@ -623,13 +683,20 @@ dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultTyp
|
||||||
|
|
||||||
--- Command specific checks
|
--- Command specific checks
|
||||||
|
|
||||||
|
-- Compare a command to a string: t `isCommand` "sed" (also matches /usr/bin/sed)
|
||||||
isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)
|
isCommand token str = isCommandMatch token (\cmd -> cmd == str || ('/' : str) `isSuffixOf` cmd)
|
||||||
|
|
||||||
|
-- Compare a command to a literal. Like above, but checks full path.
|
||||||
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
isUnqualifiedCommand token str = isCommandMatch token (== str)
|
||||||
|
|
||||||
isCommandMatch token matcher = fromMaybe False $ do
|
isCommandMatch token matcher = fromMaybe False $ do
|
||||||
cmd <- getCommandName token
|
cmd <- getCommandName token
|
||||||
return $ matcher cmd
|
return $ matcher cmd
|
||||||
|
|
||||||
|
-- Does this regex look like it was intended as a glob?
|
||||||
|
-- True: *foo*
|
||||||
|
-- False: .*foo.*
|
||||||
|
isConfusedGlobRegex :: String -> Bool
|
||||||
isConfusedGlobRegex ('*':_) = True
|
isConfusedGlobRegex ('*':_) = True
|
||||||
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
|
isConfusedGlobRegex [x,'*'] | x /= '\\' = True
|
||||||
isConfusedGlobRegex _ = False
|
isConfusedGlobRegex _ = False
|
||||||
|
@ -656,6 +723,7 @@ getVariablesFromLiteral string =
|
||||||
where
|
where
|
||||||
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
||||||
|
|
||||||
|
-- Get the variable name from an expansion like ${var:-foo}
|
||||||
prop_getBracedReference1 = getBracedReference "foo" == "foo"
|
prop_getBracedReference1 = getBracedReference "foo" == "foo"
|
||||||
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
|
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
|
||||||
prop_getBracedReference3 = getBracedReference "#" == "#"
|
prop_getBracedReference3 = getBracedReference "#" == "#"
|
||||||
|
@ -706,13 +774,22 @@ getBracedModifier s = fromMaybe "" . listToMaybe $ do
|
||||||
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
|
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
|
||||||
dropModifier x = [x]
|
dropModifier x = [x]
|
||||||
|
|
||||||
-- Useful generic functions
|
-- Useful generic functions.
|
||||||
|
|
||||||
|
-- Run an action in a Maybe (or do nothing).
|
||||||
|
-- Example:
|
||||||
|
-- potentially $ do
|
||||||
|
-- s <- getLiteralString cmd
|
||||||
|
-- guard $ s `elem` ["--recursive", "-r"]
|
||||||
|
-- return $ warn .. "Something something recursive"
|
||||||
potentially :: Monad m => Maybe (m ()) -> m ()
|
potentially :: Monad m => Maybe (m ()) -> m ()
|
||||||
potentially = fromMaybe (return ())
|
potentially = fromMaybe (return ())
|
||||||
|
|
||||||
|
-- Get element 0 or a default. Like `head` but safe.
|
||||||
headOrDefault _ (a:_) = a
|
headOrDefault _ (a:_) = a
|
||||||
headOrDefault def _ = def
|
headOrDefault def _ = def
|
||||||
|
|
||||||
|
--- Get element n of a list, or Nothing. Like `!!` but safe.
|
||||||
(!!!) list i =
|
(!!!) list i =
|
||||||
case drop i list of
|
case drop i list of
|
||||||
[] -> Nothing
|
[] -> Nothing
|
||||||
|
@ -752,8 +829,10 @@ isCountingReference _ = False
|
||||||
isQuotedAlternativeReference t =
|
isQuotedAlternativeReference t =
|
||||||
case t of
|
case t of
|
||||||
T_DollarBraced _ _ ->
|
T_DollarBraced _ _ ->
|
||||||
":+" `isInfixOf` bracedString t
|
getBracedModifier (bracedString t) `matches` re
|
||||||
_ -> False
|
_ -> False
|
||||||
|
where
|
||||||
|
re = mkRegex "(^|\\]):?\\+"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ import Control.Monad.RWS
|
||||||
import Data.Char
|
import Data.Char
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map.Strict as Map
|
||||||
import Test.QuickCheck.All (forAllProperties)
|
import Test.QuickCheck.All (forAllProperties)
|
||||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||||
|
|
||||||
|
@ -85,13 +85,15 @@ commandChecks = [
|
||||||
,checkDeprecatedTempfile
|
,checkDeprecatedTempfile
|
||||||
,checkDeprecatedEgrep
|
,checkDeprecatedEgrep
|
||||||
,checkDeprecatedFgrep
|
,checkDeprecatedFgrep
|
||||||
|
,checkWhileGetoptsCase
|
||||||
|
,checkCatastrophicRm
|
||||||
]
|
]
|
||||||
|
|
||||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||||
buildCommandMap = foldl' addCheck Map.empty
|
buildCommandMap = foldl' addCheck Map.empty
|
||||||
where
|
where
|
||||||
addCheck map (CommandCheck name function) =
|
addCheck map (CommandCheck name function) =
|
||||||
Map.insertWith' composeAnalyzers name function map
|
Map.insertWith composeAnalyzers name function map
|
||||||
|
|
||||||
|
|
||||||
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
||||||
|
@ -385,17 +387,26 @@ prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b"
|
||||||
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
|
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
|
||||||
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
|
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
|
||||||
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
|
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
|
||||||
|
prop_checkMkdirDashPM15 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../bin"
|
||||||
|
prop_checkMkdirDashPM16 = verify checkMkdirDashPM "mkdir -p -m 0755 ../bin/laden"
|
||||||
|
prop_checkMkdirDashPM17 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./bin"
|
||||||
|
prop_checkMkdirDashPM18 = verify checkMkdirDashPM "mkdir -p -m 0755 ./bin/laden"
|
||||||
|
prop_checkMkdirDashPM19 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./../bin"
|
||||||
|
prop_checkMkdirDashPM20 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin"
|
||||||
|
prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin"
|
||||||
checkMkdirDashPM = CommandCheck (Basename "mkdir") check
|
checkMkdirDashPM = CommandCheck (Basename "mkdir") check
|
||||||
where
|
where
|
||||||
check t = potentially $ do
|
check t = potentially $ do
|
||||||
let flags = getAllFlags t
|
let flags = getAllFlags t
|
||||||
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
|
dashP <- find ((\f -> f == "p" || f == "parents") . snd) flags
|
||||||
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
|
dashM <- find ((\f -> f == "m" || f == "mode") . snd) flags
|
||||||
guard $ any couldHaveSubdirs (drop 1 $ arguments t) -- mkdir -pm 0700 dir is fine, but dir/subdir is not.
|
-- mkdir -pm 0700 dir is fine, so is ../dir, but dir/subdir is not.
|
||||||
|
guard $ any couldHaveSubdirs (drop 1 $ arguments t)
|
||||||
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
|
return $ warn (getId $ fst dashM) 2174 "When used with -p, -m only applies to the deepest directory."
|
||||||
couldHaveSubdirs t = fromMaybe True $ do
|
couldHaveSubdirs t = fromMaybe True $ do
|
||||||
name <- getLiteralString t
|
name <- getLiteralString t
|
||||||
return $ '/' `elem` name
|
return $ '/' `elem` name && not (name `matches` re)
|
||||||
|
re = mkRegex "^(\\.\\.?\\/)+[^/]+$"
|
||||||
|
|
||||||
|
|
||||||
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
|
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8"
|
||||||
|
@ -681,5 +692,130 @@ prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files"
|
||||||
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
|
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
|
||||||
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
|
\t -> info (getId t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
|
||||||
|
|
||||||
|
prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
|
||||||
|
prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"
|
||||||
|
prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; esac; done"
|
||||||
|
prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done"
|
||||||
|
prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"
|
||||||
|
checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
||||||
|
where
|
||||||
|
f :: Token -> Analysis
|
||||||
|
f t@(T_SimpleCommand _ _ (cmd:arg1:_)) = do
|
||||||
|
path <- getPathM t
|
||||||
|
potentially $ do
|
||||||
|
options <- getLiteralString arg1
|
||||||
|
(T_WhileExpression _ _ body) <- findFirst whileLoop path
|
||||||
|
caseCmd <- mapMaybe findCase body !!! 0
|
||||||
|
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd
|
||||||
|
f _ = return ()
|
||||||
|
|
||||||
|
check :: Id -> [String] -> Token -> Analysis
|
||||||
|
check optId opts (T_CaseExpression id _ list) = do
|
||||||
|
unless (Nothing `Map.member` handledMap) $
|
||||||
|
mapM_ (warnUnhandled optId id) $ catMaybes $ Map.keys notHandled
|
||||||
|
|
||||||
|
mapM_ warnRedundant $ Map.toList notRequested
|
||||||
|
|
||||||
|
where
|
||||||
|
handledMap = Map.fromList (concatMap getHandledStrings list)
|
||||||
|
requestedMap = Map.fromList $ map (\x -> (Just x, ())) opts
|
||||||
|
|
||||||
|
notHandled = Map.difference requestedMap handledMap
|
||||||
|
notRequested = Map.difference handledMap requestedMap
|
||||||
|
|
||||||
|
warnUnhandled optId caseId str =
|
||||||
|
warn caseId 2213 $ "getopts specified -" ++ str ++ ", but it's not handled by this 'case'."
|
||||||
|
|
||||||
|
warnRedundant (key, expr) = potentially $ do
|
||||||
|
str <- key
|
||||||
|
guard $ str `notElem` ["*", ":", "?"]
|
||||||
|
return $ warn (getId expr) 2214 "This case is not specified by getopts."
|
||||||
|
|
||||||
|
getHandledStrings (_, globs, _) =
|
||||||
|
map (\x -> (literal x, x)) globs
|
||||||
|
|
||||||
|
literal :: Token -> Maybe String
|
||||||
|
literal t = do
|
||||||
|
getLiteralString t <> fromGlob t
|
||||||
|
|
||||||
|
fromGlob t =
|
||||||
|
case t of
|
||||||
|
T_Glob _ ('[':c:']':[]) -> return [c]
|
||||||
|
T_Glob _ "*" -> return "*"
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
whileLoop t =
|
||||||
|
case t of
|
||||||
|
T_WhileExpression {} -> return True
|
||||||
|
T_Script {} -> return False
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
findCase t =
|
||||||
|
case t of
|
||||||
|
T_Annotation _ _ x -> findCase x
|
||||||
|
T_Pipeline _ _ [x] -> findCase x
|
||||||
|
T_Redirecting _ _ x@(T_CaseExpression {}) -> return x
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2"
|
||||||
|
prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo"
|
||||||
|
prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"
|
||||||
|
prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"
|
||||||
|
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
|
||||||
|
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
|
||||||
|
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
|
||||||
|
prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
|
||||||
|
prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
|
||||||
|
prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
|
||||||
|
prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
|
||||||
|
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
|
||||||
|
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
|
||||||
|
checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
||||||
|
when (isRecursive t) $
|
||||||
|
mapM_ (mapM_ checkWord . braceExpand) $ arguments t
|
||||||
|
where
|
||||||
|
isRecursive = any (`elem` ["r", "R", "recursive"]) . map snd . getAllFlags
|
||||||
|
|
||||||
|
checkWord token =
|
||||||
|
case getLiteralString token of
|
||||||
|
Just str ->
|
||||||
|
when (fixPath str `elem` importantPaths) $
|
||||||
|
warn (getId token) 2114 "Warning: deletes a system directory."
|
||||||
|
Nothing ->
|
||||||
|
checkWord' token
|
||||||
|
|
||||||
|
checkWord' token = fromMaybe (return ()) $ do
|
||||||
|
filename <- getPotentialPath token
|
||||||
|
let path = fixPath filename
|
||||||
|
return . when (path `elem` importantPaths) $
|
||||||
|
warn (getId token) 2115 $ "Use \"${var:?}\" to ensure this never expands to " ++ path ++ " ."
|
||||||
|
|
||||||
|
fixPath filename =
|
||||||
|
let normalized = skipRepeating '/' . skipRepeating '*' $ filename in
|
||||||
|
if normalized == "/" then normalized else stripTrailing '/' normalized
|
||||||
|
|
||||||
|
getPotentialPath = getLiteralStringExt f
|
||||||
|
where
|
||||||
|
f (T_Glob _ str) = return str
|
||||||
|
f (T_DollarBraced _ word) =
|
||||||
|
let var = onlyLiteralString word in
|
||||||
|
-- This shouldn't handle non-colon cases.
|
||||||
|
if any (`isInfixOf` var) [":?", ":-", ":="]
|
||||||
|
then Nothing
|
||||||
|
else return ""
|
||||||
|
f _ = return ""
|
||||||
|
|
||||||
|
stripTrailing c = reverse . dropWhile (== c) . reverse
|
||||||
|
skipRepeating c (a:b:rest) | a == b && b == c = skipRepeating c (b:rest)
|
||||||
|
skipRepeating c (a:r) = a:skipRepeating c r
|
||||||
|
skipRepeating _ [] = []
|
||||||
|
|
||||||
|
paths = [
|
||||||
|
"", "/bin", "/etc", "/home", "/mnt", "/usr", "/usr/share", "/usr/local",
|
||||||
|
"/var", "/lib", "/dev", "/media", "/boot", "/lib64", "/usr/bin"
|
||||||
|
]
|
||||||
|
importantPaths = filter (not . null) $
|
||||||
|
["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths)
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||||
|
|
|
@ -440,19 +440,9 @@ readConditionContents single =
|
||||||
|
|
||||||
getOp = do
|
getOp = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
op <- anyQuotedOp <|> anyEscapedOp <|> anyOp
|
op <- readRegularOrEscaped anyOp
|
||||||
return $ TC_Binary id typ op
|
return $ TC_Binary id typ op
|
||||||
|
|
||||||
-- hacks to read quoted operators without having to read a shell word
|
|
||||||
anyEscapedOp = try $ do
|
|
||||||
char '\\'
|
|
||||||
escaped <$> anyOp
|
|
||||||
anyQuotedOp = try $ do
|
|
||||||
c <- oneOf "'\""
|
|
||||||
s <- anyOp
|
|
||||||
char c
|
|
||||||
return $ escaped s
|
|
||||||
|
|
||||||
anyOp = flagOp <|> flaglessOp <|> fail
|
anyOp = flagOp <|> flaglessOp <|> fail
|
||||||
"Expected comparison operator (don't wrap commands in []/[[]])"
|
"Expected comparison operator (don't wrap commands in []/[[]])"
|
||||||
flagOp = try $ do
|
flagOp = try $ do
|
||||||
|
@ -461,7 +451,22 @@ readConditionContents single =
|
||||||
return s
|
return s
|
||||||
flaglessOp =
|
flaglessOp =
|
||||||
choice $ map (try . string) flaglessOps
|
choice $ map (try . string) flaglessOps
|
||||||
escaped s = if any (`elem` s) "<>" then '\\':s else s
|
|
||||||
|
-- hacks to read quoted operators without having to read a shell word
|
||||||
|
readEscaped p = try $ withEscape <|> withQuotes
|
||||||
|
where
|
||||||
|
withEscape = do
|
||||||
|
char '\\'
|
||||||
|
escaped <$> p
|
||||||
|
withQuotes = do
|
||||||
|
c <- oneOf "'\""
|
||||||
|
s <- p
|
||||||
|
char c
|
||||||
|
return $ escaped s
|
||||||
|
escaped s = if any (`elem` s) "<>()" then '\\':s else s
|
||||||
|
|
||||||
|
readRegularOrEscaped p = readEscaped p <|> p
|
||||||
|
|
||||||
|
|
||||||
guardArithmetic = do
|
guardArithmetic = do
|
||||||
try . lookAhead $ disregard (oneOf "+*/%") <|> disregard (string "- ")
|
try . lookAhead $ disregard (oneOf "+*/%") <|> disregard (string "- ")
|
||||||
|
@ -560,29 +565,30 @@ readConditionContents single =
|
||||||
"You need a space before and after the " ++ trailingOp ++ " ."
|
"You need a space before and after the " ++ trailingOp ++ " ."
|
||||||
|
|
||||||
readCondGroup = do
|
readCondGroup = do
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
lparen <- try $ string "(" <|> string "\\("
|
lparen <- try $ readRegularOrEscaped (string "(")
|
||||||
when (single && lparen == "(") $
|
when (single && lparen == "(") $
|
||||||
parseProblemAt pos ErrorC 1028 "In [..] you have to escape (). Use [[..]] instead."
|
singleWarning pos
|
||||||
when (not single && lparen == "\\(") $
|
when (not single && lparen == "\\(") $
|
||||||
parseProblemAt pos ErrorC 1029 "In [[..]] you shouldn't escape ()."
|
doubleWarning pos
|
||||||
condSpacing single
|
condSpacing single
|
||||||
x <- readCondContents
|
x <- readCondContents
|
||||||
cpos <- getPosition
|
cpos <- getPosition
|
||||||
rparen <- string ")" <|> string "\\)"
|
rparen <- readRegularOrEscaped (string ")")
|
||||||
condSpacing single
|
condSpacing single
|
||||||
when (single && rparen == ")") $
|
when (single && rparen == ")") $
|
||||||
parseProblemAt cpos ErrorC 1030 "In [..] you have to escape (). Use [[..]] instead."
|
singleWarning cpos
|
||||||
when (not single && rparen == "\\)") $
|
when (not single && rparen == "\\)") $
|
||||||
parseProblemAt cpos ErrorC 1031 "In [[..]] you shouldn't escape ()."
|
doubleWarning cpos
|
||||||
when (isEscaped lparen `xor` isEscaped rparen) $
|
return $ TC_Group id typ x
|
||||||
parseProblemAt pos ErrorC 1032 "Did you just escape one half of () but not the other?"
|
|
||||||
return $ TC_Group id typ x
|
|
||||||
where
|
where
|
||||||
isEscaped ('\\':_) = True
|
singleWarning pos =
|
||||||
isEscaped _ = False
|
parseProblemAt pos ErrorC 1028 "In [..] you have to escape \\( \\) or preferably combine [..] expressions."
|
||||||
xor x y = x && not y || not x && y
|
doubleWarning pos =
|
||||||
|
parseProblemAt pos ErrorC 1029 "In [[..]] you shouldn't escape ( or )."
|
||||||
|
|
||||||
|
|
||||||
-- Currently a bit of a hack since parsing rules are obscure
|
-- Currently a bit of a hack since parsing rules are obscure
|
||||||
regexOperatorAhead = lookAhead (do
|
regexOperatorAhead = lookAhead (do
|
||||||
|
@ -849,6 +855,7 @@ prop_readCondition15= isOk readCondition "[ foo \">=\" bar ]"
|
||||||
prop_readCondition16= isOk readCondition "[ foo \\< bar ]"
|
prop_readCondition16= isOk readCondition "[ foo \\< bar ]"
|
||||||
prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"
|
prop_readCondition17= isOk readCondition "[[ ${file::1} = [-.\\|/\\\\] ]]"
|
||||||
prop_readCondition18= isOk readCondition "[ ]"
|
prop_readCondition18= isOk readCondition "[ ]"
|
||||||
|
prop_readCondition19= isOk readCondition "[ '(' x \")\" ]"
|
||||||
readCondition = called "test expression" $ do
|
readCondition = called "test expression" $ do
|
||||||
opos <- getPosition
|
opos <- getPosition
|
||||||
id <- getNextId
|
id <- getNextId
|
||||||
|
@ -889,10 +896,13 @@ prop_readAnnotation1 = isOk readAnnotation "# shellcheck disable=1234,5678\n"
|
||||||
prop_readAnnotation2 = isOk readAnnotation "# shellcheck disable=SC1234 disable=SC5678\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_readAnnotation3 = isOk readAnnotation "# shellcheck disable=SC1234 source=/dev/null disable=SC5678\n"
|
||||||
prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n"
|
prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=SC1234\n"
|
||||||
|
prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n"
|
||||||
|
prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
|
||||||
readAnnotation = called "shellcheck annotation" $ do
|
readAnnotation = called "shellcheck annotation" $ do
|
||||||
try readAnnotationPrefix
|
try readAnnotationPrefix
|
||||||
many1 linewhitespace
|
many1 linewhitespace
|
||||||
values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride <|> anyKey)
|
values <- many1 (readDisable <|> readSourceOverride <|> readShellOverride <|> anyKey)
|
||||||
|
optional readAnyComment
|
||||||
linefeed
|
linefeed
|
||||||
many linewhitespace
|
many linewhitespace
|
||||||
return $ concat values
|
return $ concat values
|
||||||
|
@ -926,7 +936,8 @@ readAnnotation = called "shellcheck annotation" $ do
|
||||||
|
|
||||||
anyKey = do
|
anyKey = do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
anyChar `reluctantlyTill1` whitespace
|
noneOf "#\r\n"
|
||||||
|
anyChar `reluctantlyTill` whitespace
|
||||||
many linewhitespace
|
many linewhitespace
|
||||||
parseNoteAt pos WarningC 1107 "This directive is unknown. It will be ignored."
|
parseNoteAt pos WarningC 1107 "This directive is unknown. It will be ignored."
|
||||||
return []
|
return []
|
||||||
|
@ -937,6 +948,9 @@ readAnnotations = do
|
||||||
|
|
||||||
readComment = do
|
readComment = do
|
||||||
unexpecting "shellcheck annotation" readAnnotationPrefix
|
unexpecting "shellcheck annotation" readAnnotationPrefix
|
||||||
|
readAnyComment
|
||||||
|
|
||||||
|
readAnyComment = do
|
||||||
char '#'
|
char '#'
|
||||||
many $ noneOf "\r\n"
|
many $ noneOf "\r\n"
|
||||||
|
|
||||||
|
@ -2729,14 +2743,18 @@ readScript = do
|
||||||
script <- readScriptFile
|
script <- readScriptFile
|
||||||
reparseIndices script
|
reparseIndices script
|
||||||
|
|
||||||
isWarning p s = parsesCleanly p s == Just False
|
|
||||||
isOk p s = parsesCleanly p s == Just True
|
|
||||||
isNotOk p s = parsesCleanly p s == Nothing
|
|
||||||
|
|
||||||
testParse string = runIdentity $ do
|
-- Interactively run a parser in ghci:
|
||||||
(res, _) <- runParser (mockedSystemInterface []) readScript "-" string
|
-- debugParse readScript "echo 'hello world'"
|
||||||
|
debugParse p string = runIdentity $ do
|
||||||
|
(res, _) <- runParser (mockedSystemInterface []) p "-" string
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
isOk p s = parsesCleanly p s == Just True -- The string parses with no warnings
|
||||||
|
isWarning p s = parsesCleanly p s == Just False -- The string parses with warnings
|
||||||
|
isNotOk p s = parsesCleanly p s == Nothing -- The string does not parse
|
||||||
|
|
||||||
parsesCleanly parser string = runIdentity $ do
|
parsesCleanly parser string = runIdentity $ do
|
||||||
(res, sys) <- runParser (mockedSystemInterface [])
|
(res, sys) <- runParser (mockedSystemInterface [])
|
||||||
(parser >> eof >> getState) "-" string
|
(parser >> eof >> getState) "-" string
|
||||||
|
@ -2745,6 +2763,16 @@ parsesCleanly parser string = runIdentity $ do
|
||||||
return $ Just . null $ parseNotes userState ++ parseProblems systemState
|
return $ Just . null $ parseNotes userState ++ parseProblems systemState
|
||||||
(Left _, _) -> return Nothing
|
(Left _, _) -> return Nothing
|
||||||
|
|
||||||
|
-- For printf debugging: print the value of an expression
|
||||||
|
-- Example: return $ dump $ T_Literal id [c]
|
||||||
|
dump :: Show a => a -> a
|
||||||
|
dump x = trace (show x) x
|
||||||
|
|
||||||
|
-- Like above, but print a specific expression:
|
||||||
|
-- Example: return $ dumps ("Returning: " ++ [c]) $ T_Literal id [c]
|
||||||
|
dumps :: Show x => x -> a -> a
|
||||||
|
dumps t = trace (show t)
|
||||||
|
|
||||||
parseWithNotes parser = do
|
parseWithNotes parser = do
|
||||||
item <- parser
|
item <- parser
|
||||||
state <- getState
|
state <- getState
|
||||||
|
@ -2877,9 +2905,6 @@ parseScript sys spec =
|
||||||
parseShell sys (psFilename spec) (psScript spec)
|
parseShell sys (psFilename spec) (psScript spec)
|
||||||
|
|
||||||
|
|
||||||
lt x = trace (show x) x
|
|
||||||
ltt t = trace (show t)
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $quickCheckAll
|
runTests = $quickCheckAll
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue