Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions src/ShellCheck/Analytics.hs
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,13 @@ optionalTreeChecks = [
cdPositive = "set -e; func() { cp *.txt ~/backup; rm *.txt; }; func && echo ok",
cdNegative = "set -e; func() { cp *.txt ~/backup; rm *.txt; }; func; echo ok"
}, checkSetESuppressed)

,(newCheckDescription {
cdName = "check-extra-masked-returns",
cdDescription = "Check for additional cases where exit codes are masked",
cdPositive = "rm -r \"$(get_chroot_dir)/home\"",
cdNegative = "set -e; dir=\"$(get_chroot_dir)\"; rm -r \"$dir/home\""
}, checkExtraMaskedReturns)
]

optionalCheckMap :: Map.Map String (Parameters -> Token -> [TokenComment])
Expand Down Expand Up @@ -4734,5 +4741,107 @@ checkSetESuppressed params t =
isIn t cmds = getId t `elem` map getId cmds


prop_checkExtraMaskedReturns1 = verifyTree checkExtraMaskedReturns "cat < <(ls)"
prop_checkExtraMaskedReturns2 = verifyTree checkExtraMaskedReturns "read -ra arr <(ls)"
prop_checkExtraMaskedReturns3 = verifyTree checkExtraMaskedReturns "ls >(cat)"
prop_checkExtraMaskedReturns4 = verifyTree checkExtraMaskedReturns "false | true"
prop_checkExtraMaskedReturns5 = verifyNotTree checkExtraMaskedReturns "set -o pipefail; false | true"
prop_checkExtraMaskedReturns6 = verifyNotTree checkExtraMaskedReturns "false | true || true"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could also include a check for { false || true; } | true to make sure you can suppress individual stages in the pipeline too (it already works as intended)

prop_checkExtraMaskedReturns7 = verifyTree checkExtraMaskedReturns "true $(false)"
prop_checkExtraMaskedReturns8 = verifyTree checkExtraMaskedReturns "x=$(false)$(true)"
prop_checkExtraMaskedReturns9 = verifyNotTree checkExtraMaskedReturns "x=$(false)true"
prop_checkExtraMaskedReturns10 = verifyTree checkExtraMaskedReturns "x=`false``false`"
prop_checkExtraMaskedReturns11 = verifyTree checkExtraMaskedReturns "x=\"$(false)$(true)\""
prop_checkExtraMaskedReturns12 = verifyTree checkExtraMaskedReturns "x=\"$(false)\"\"$(true)\""
prop_checkExtraMaskedReturns13 = verifyTree checkExtraMaskedReturns "true <<<$(false)"
prop_checkExtraMaskedReturns14 = verifyNotTree checkExtraMaskedReturns "echo asdf | false"
prop_checkExtraMaskedReturns15 = verifyNotTree checkExtraMaskedReturns "readonly x=$(false)"
prop_checkExtraMaskedReturns16 = verifyTree checkExtraMaskedReturns "readarray -t files < <(ls)"
prop_checkExtraMaskedReturns17 = verifyNotTree checkExtraMaskedReturns "x=( $(false) false )"
prop_checkExtraMaskedReturns18 = verifyTree checkExtraMaskedReturns "x=( $(false) $(false) )"
prop_checkExtraMaskedReturns19 = verifyNotTree checkExtraMaskedReturns "x=( $(false) [4]=false )"
prop_checkExtraMaskedReturns20 = verifyTree checkExtraMaskedReturns "x=( $(false) [4]=$(false) )"
prop_checkExtraMaskedReturns21 = verifyTree checkExtraMaskedReturns "cat << foo\n $(false)\nfoo"
prop_checkExtraMaskedReturns22 = verifyTree checkExtraMaskedReturns "[[ $(false) ]]"
prop_checkExtraMaskedReturns23 = verifyNotTree checkExtraMaskedReturns "x=$(false) y=z"
prop_checkExtraMaskedReturns24 = verifyNotTree checkExtraMaskedReturns "x=$(( $(date +%s) ))"

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out I was wrong about this one, sorry about that:

$ x=$(( $(echo 1; exit 42) )); echo $?
42

prop_checkExtraMaskedReturns25 = verifyTree checkExtraMaskedReturns "echo $(( $(date +%s) ))"
prop_checkExtraMaskedReturns26 = verifyNotTree checkExtraMaskedReturns "x=( $(false) )"
prop_checkExtraMaskedReturns27 = verifyTree checkExtraMaskedReturns "x=$(false) false"
prop_checkExtraMaskedReturns28 = verifyTree checkExtraMaskedReturns "x=$(false) y=$(false)"
prop_checkExtraMaskedReturns29 = verifyNotTree checkExtraMaskedReturns "false < <(set -e)"
prop_checkExtraMaskedReturns30 = verifyNotTree checkExtraMaskedReturns "false < <(shopt -s cdspell)"
prop_checkExtraMaskedReturns31 = verifyNotTree checkExtraMaskedReturns "false < <(dirname \"${BASH_SOURCE[0]}\")"
prop_checkExtraMaskedReturns32 = verifyNotTree checkExtraMaskedReturns "false < <(basename \"${BASH_SOURCE[0]}\")"
prop_checkExtraMaskedReturns33 = verifyNotTree checkExtraMaskedReturns "{ false || true; } | true"
prop_checkExtraMaskedReturns34 = verifyNotTree checkExtraMaskedReturns "{ false || :; } | true"
checkExtraMaskedReturns params t = runNodeAnalysis findMaskingNodes params t
where
findMaskingNodes _ (T_Arithmetic _ list) = findMaskedNodesInList [list]
findMaskingNodes _ (T_Array _ list) = findMaskedNodesInList $ allButLastSimpleCommands list
findMaskingNodes _ (T_Condition _ _ condition) = findMaskedNodesInList [condition]
findMaskingNodes _ (T_DoubleQuoted _ list) = findMaskedNodesInList $ allButLastSimpleCommands list
findMaskingNodes _ (T_HereDoc _ _ _ _ list) = findMaskedNodesInList list
findMaskingNodes _ (T_HereString _ word) = findMaskedNodesInList [word]
findMaskingNodes _ (T_NormalWord _ parts) = findMaskedNodesInList $ allButLastSimpleCommands parts
findMaskingNodes _ (T_Pipeline _ _ cmds) | not (hasPipefail params) = findMaskedNodesInList $ allButLastSimpleCommands cmds
findMaskingNodes _ (T_ProcSub _ _ list) = findMaskedNodesInList list
findMaskingNodes _ (T_SimpleCommand _ assigns (_:args)) = findMaskedNodesInList $ assigns ++ args
findMaskingNodes _ (T_SimpleCommand _ assigns []) = findMaskedNodesInList $ allButLastSimpleCommands assigns
findMaskingNodes _ _ = return ()

findMaskedNodesInList = mapM_ (doAnalysis findMaskedNodes)

isMaskedNode t = not (isHarmlessCommand t || isCheckedElsewhere t || isMaskDeliberate t)
findMaskedNodes t@(T_SimpleCommand _ _ (_:_)) = when (isMaskedNode t) $ inform t
findMaskedNodes t@T_Condition {} = when (isMaskedNode t) $ inform t
findMaskedNodes _ = return ()

containsSimpleCommand t = isNothing $ doAnalysis go t
where
go t = case t of
T_SimpleCommand {} -> fail ""
_ -> return ()

allButLastSimpleCommands cmds =
if null simpleCommands then [] else init simpleCommands
where
simpleCommands = filter containsSimpleCommand cmds

inform t = info (getId t) 2312 ("Consider invoking this command "
++ "separately to avoid masking its return value (or use '|| true' "
++ "to ignore).")

isMaskDeliberate t = hasParent isOrIf t
where
isOrIf _ (T_OrIf _ _ (T_Pipeline _ _ [T_Redirecting _ _ cmd]))
= getCommandBasename cmd `elem` [Just "true", Just ":"]
isOrIf _ _ = False

isCheckedElsewhere t = hasParent isDeclaringCommand t
where
isDeclaringCommand t _ = fromMaybe False $ do
basename <- getCommandBasename t
return $ basename `elem` declaringCommands

isHarmlessCommand t = fromMaybe False $ do
basename <- getCommandBasename t
return $ basename `elem` [
"echo"
,"basename"
,"dirname"
,"printf"
,"set"
,"shopt"
]

parentChildPairs t = go $ parents params t
where
go (child:parent:rest) = (parent, child):go (parent:rest)
go _ = []

hasParent pred t = any (uncurry pred) (parentChildPairs t)


return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
18 changes: 18 additions & 0 deletions src/ShellCheck/AnalyzerLib.hs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ data Parameters = Parameters {
hasInheritErrexit :: Bool,
-- Whether this script has 'set -e' anywhere.
hasSetE :: Bool,
-- Whether this script has 'set -o pipefail' anywhere.
hasPipefail :: Bool,
-- A linear (bad) analysis of data flow
variableFlow :: [StackData],
-- A map from Id to parent Token
Expand Down Expand Up @@ -204,6 +206,12 @@ makeParameters spec =
Dash -> True
Sh -> True
Ksh -> False,
hasPipefail =
case shellType params of
Bash -> containsPipefail root
Dash -> True
Sh -> True
Ksh -> containsPipefail root,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
parentMap = getParentTree root,
variableFlow = getVariableFlow params root,
Expand All @@ -226,6 +234,16 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
_ -> False
re = mkRegex "[[:space:]]-[^-]*e"

containsPipefail root = isNothing $ doAnalysis (guard . not . isPipefail) root
where
isPipefail t =
case t of
T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" &&
("pipefail" `elem` oversimplify t ||
"o" `elem` map snd (getAllFlags t))
_ -> False

containsShopt shopt root =
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
where
Expand Down
2 changes: 0 additions & 2 deletions src/ShellCheck/Checks/Commands.hs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ commandChecks = [
++ map checkArgComparison declaringCommands
++ map checkMaskedReturns declaringCommands

declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]


optionalChecks = map fst optionalCommandChecks
optionalCommandChecks :: [(CheckDescription, CommandCheck)]
Expand Down
2 changes: 2 additions & 0 deletions src/ShellCheck/Data.hs
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,5 @@ shellForExecutable name =
_ -> Nothing

flagsForRead = "sreu:n:N:i:p:a:t:"

declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]