Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add extra checks for masked return codes #2320

Merged
merged 1 commit into from
Oct 2, 2021
Merged

Add extra checks for masked return codes #2320

merged 1 commit into from
Oct 2, 2021

Conversation

DoxasticFox
Copy link
Contributor

@DoxasticFox DoxasticFox commented Sep 5, 2021

Adds the check mentioned here, plus related ones: #2303 (comment) Example:

/tmp/a.sh:

#!/usr/bin/env bash
set -e
readarray -t files < <(ls)
echo "${files[@]}"
% ./quickrun /tmp/a.sh --enable=check-extra-masked-returns
Up to date

In /tmp/a.sh line 3:
readarray -t files < <(ls)
                       ^-- SC2312: Consider invoking this command separately to avoid masking its return value (or use '|| true' to ignore).

For more information:
  https://www.shellcheck.net/wiki/SC2312 -- Consider invoking this command se...

@DoxasticFox
Copy link
Contributor Author

CC @uri-canva

@koalaman
Copy link
Owner

koalaman commented Sep 9, 2021

I think this would need to cover quite a bit more to live up to its name, such as false | true, true $(false), and x=$(false)$(true), and a check specifically for process substitutions in scripts containing set -e seems a bit narrow on its own.

@DoxasticFox
Copy link
Contributor Author

@koalaman I think it highlights another issue with my chosen name: It's that the check isn't related to set -e so much as it is to masking return values (e.g. as in SC2155). So although set -e makes Bash's treatment of the masked exit codes even more unexpected, I'd still say it's not the core issue. How about I rename check-set-e-ignored to check-masked-exit-codes, or check-extra-masked-exit-codes (seeing as shellcheck already deals with one case of masking in SC2155). I'll also add checks for the cases you mention, being careful to avoid the cases already handled by SC2155. SGTY?

@koalaman
Copy link
Owner

SGTM. In this case it should recognize || true so that you have an easy escape hatch for things you're willing to assume will succeed, such as x $(y || true) and x < <(y || true).

@DoxasticFox DoxasticFox changed the title Show info about set -e being ignored in process substitutions Add extra checks for masked return codes Sep 25, 2021
@DoxasticFox
Copy link
Contributor Author

@koalaman PTAL


isMaskDeliberate t = hasParent isOrIf t
where
isOrIf _ (T_OrIf _ _ right) = ["true"] == oversimplify right
Copy link
Owner

Choose a reason for hiding this comment

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

oversimplify wasn't as clever or useful as I thought it would be 10 years ago. It would be better to use getCommandBasename :: Token -> Maybe String which is smarter and less heavy handed.

@@ -24,6 +24,7 @@ module ShellCheck.Analytics (runAnalytics, optionalChecks, ShellCheck.Analytics.
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.AnalyzerLib hiding (producesComments)
import ShellCheck.Checks.Commands (declaringCommands)
Copy link
Owner

Choose a reason for hiding this comment

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

You could move declaringCommands into ShellCheck.Data instead to avoid this dependency and hopefully allow better build parallelization.

| isMaskDeliberate t = return ()
| isCheckedElsewhere t = return ()
| isMasked t = info (getId t) 2312
"Invoke command separately to avoid masking its return value."
Copy link
Owner

Choose a reason for hiding this comment

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

Maybe add " (or use || true to ignore)."

Invoking separately is not necessarily the best advice for pipes but the wiki can go into detail.

|| isMaskedInHereString t
|| isMaskedInPipe t
|| isMaskedInProcSub t
|| isMaskedInWord t
Copy link
Owner

Choose a reason for hiding this comment

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

You could instead formulate this as something like

isMasked t = hasParent masker t
  where
    masker t =
      isHereString t
      || hasRightSibblingInPipe t
      || ..

to avoid iterating the same path repeatedly and instead do it in one pass.

prop_checkExtraMaskedReturns9 = verifyNotTree checkExtraMaskedReturns "echo asdf | false"
prop_checkExtraMaskedReturns10 = verifyNotTree checkExtraMaskedReturns "readonly x=$(false)"
prop_checkExtraMaskedReturns11 = verifyTree checkExtraMaskedReturns "readarray -t files < <(ls)"
checkExtraMaskedReturns params t = runNodeAnalysis checkNode params t
Copy link
Owner

Choose a reason for hiding this comment

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

So far so good! Here are some cases it misses:

False negative:

x="$(false)$(false)"      # Check out getWordParts :: Token -> [Token]
x="$(false)""$(false)"    # It takes a T_NormalWord and flattens it into a list of all the quoted and unquoted elements in the string
x=( $(false) $(false) )
x=( $(false) [4]=$(false) )

# These may be less relevant:
cat << foo
 $(false)
foo
x=$(( $(date +%s) ))
[[ $(false) ]]

False positive:

x=$(false) y=z         # T_SimpleCommand has two assignments in its list in this case
x=$(false) y=$(false)  # Only the first one is masked

These are already correctly handled but untested:

echo $(( $(date +%s) ))
x=`false``false`
x=( $(false) )

Copy link
Contributor Author

@DoxasticFox DoxasticFox Sep 26, 2021

Choose a reason for hiding this comment

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

Thanks for all these test cases. Any suggestion as to how I should approach this?

x=$(false) y=$(false)  # Only the first one is masked

I was thinking that once I find a T_SimpleCommand, I'd traverse the parse tree upwards, looking for a T_Assignment. Once I find that, I'd look at the assignments on the the left of the T_SimpleCommand and check to see if their descendants contain any T_SimpleCommand nodes. The trouble is that it's not obvious to me how I can iterate over the descendants of a node without having a huge case ... of statement which essentially handles each rule in Bash's grammar.

Copy link
Owner

Choose a reason for hiding this comment

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

This big case...of statement already exists. There's a doAnalysis which invokes each node in a tree with a monadic action. The obvious monad is Cont, but to avoid importing that, perhaps Maybe:

containsSimpleCommand rootnode = isNothing $ doAnalysis (\c -> case c of T_SimpleCommand {} -> fail ""; _ -> return ()) rootnode

Currently the code finds the potentially masked T_SimpleCommand, and tries to determine if it is masked. This has some issues like echo $(t=$(false); echo $t) emitting two warnings even though only one is actually masked, and echo $([[ x ]]) emits nothing since it's a compound command instead of a simple command.

An alternative approach is finding the potential masker, e.g. a T_ProcSub, or a T_SimpleCommand _ [] (cmd:args) where any isCommandSubstitution $ concatMap getWordParts args. Since it seems like this is basically required to recognize the context anyways, maybe this is easier.

@DoxasticFox
Copy link
Contributor Author

@koalaman How am I going?

@DoxasticFox
Copy link
Contributor Author

Looks like this PR will address #1733. Not sure if that's a good thing though; is it okay for it to be an optional check?

Copy link
Owner

@koalaman koalaman left a comment

Choose a reason for hiding this comment

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

Looks pretty good and feature complete to me!

Did you run this on a larger codebase? What was your experience?

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
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)

isMaskDeliberate t = hasParent isOrIf t
where
isOrIf _ (T_OrIf _ _ (T_Pipeline _ _ [T_Redirecting _ _ cmd]))
= getCommandBasename cmd == Just "true"
Copy link
Owner

Choose a reason for hiding this comment

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

It should probably support ":" as well since people will likely be writing this a lot :P

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
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


findMaskedNodes t | isHarmlessCommand t = return ()
findMaskedNodes t | isCheckedElsewhere t = return ()
findMaskedNodes t | isMaskDeliberate t = return ()
Copy link
Owner

Choose a reason for hiding this comment

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

It's not as pretty but you could limit these extra checks to relevant nodes with something like

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

This drops total ShellCheck-wide CPU usage by about 5%

@DoxasticFox
Copy link
Contributor Author

DoxasticFox commented Sep 29, 2021

Did you run this on a larger codebase? What was your experience?

@koalaman Yeah, that's why I added these tests:

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 "SCRIPT_DIR=\"$( cd \"$( dirname \"$0\" )\" && pwd )\""

But actually I should run it once more and eyeball the results again now that I've made those tweaks.

Also, to make the test SCRIPT_DIR=\"$( cd \"$( dirname \"$0\" )\" && pwd )\" pass, I decided to make dirname a "harmless" command. What I really would've liked was to make it harmless only when invoked on $0 or ${BASH_SOURCE[0]}, but I couldn't see an easy and obvious way to do that. Got any pithy ways to check for that, oh Haskell master? (Edit 2: I gave it a shot in 77f393e. Please let me know if there's a better way.)

Edit 1: I ran it again on our monorepo. I'm seeing lots of errors about basename "$0" in our code base. That's another place where it'd be nice to automatically suppress errors for some_command "$0", but not for other invocations of some_command. The output seems okay to me otherwise.

@koalaman
Copy link
Owner

koalaman commented Oct 1, 2021

What I really would've liked was to make it harmless only when invoked on $0 or ${BASH_SOURCE[0]}

I would avoid this kind of overfitting TBH. It's often better for the test to be predictable, even if it makes it slightly more inaccurate. "dirname and basename are ignored since they're rarely sources of failure" is simple and easy, while it's harder to build a mental model given observations like:

  • No warning: basename ${BASH_SOURCE[0]}
  • Warning: basename ${BASH_SOURCE[0]?}
  • No warning: basename $0
  • Warning: current_exe=$0; basename $current_exe
  • No warning: dirname ${BASH_SOURCE[0]}
  • Warning: dirname ${BASH_SOURCE[-1]}

Other than that it looks good to me. Squash for merge?

PS: Any thoughts on a more convincing doc example? Maybe something like

    cdPositive = "rm -r \"$(get_chroot_dir)/home\"",
    cdNegative = "set -e; dir=\"$(get_chroot_dir)\"; rm -r \"$dir/home\""

@DoxasticFox
Copy link
Contributor Author

Squashed

It's often better for the test to be predictable, even if it makes it slightly more inaccurate.

Makes sense. I reverted 77f393e (the commit to specifically silence dirname $0 and basename $0) and added basename to the list of harmless commands instead.

PS: Any thoughts on a more convincing doc example? Maybe something like [...]

👍

@koalaman koalaman merged commit 23cddb0 into koalaman:master Oct 2, 2021
@koalaman
Copy link
Owner

koalaman commented Oct 2, 2021

Thanks! I've merged it and added a wiki page and changelog entry.

@DoxasticFox
Copy link
Contributor Author

You might also want to consider closing #1733

@DoxasticFox
Copy link
Contributor Author

Hi again, @koalaman! Thanks for your responsiveness with this and #2303. @Canva is considering using these new checks in our CI pipelines, but we'd rather not go through the rigmarole of compiling our own version. Is it much trouble to ask for a release?

CC @joscha

@koalaman
Copy link
Owner

@DoxasticFox Sure. There are a few minor issues I'd like to squeeze in first, including #2362 to extend this warning to local -r x=$(false) which you might be interested in, but I can probably get a new version out next weekend if that works.

@koalaman
Copy link
Owner

koalaman commented Nov 7, 2021

@DoxasticFox v0.8.0 is now out

Kangie pushed a commit to Kangie/shellcheck that referenced this pull request Feb 27, 2023
Merge of upstream through the stable 0.8.0 release.

Conflicts:
	ShellCheck.cabal
	src/ShellCheck/Analytics.hs
	src/ShellCheck/AnalyzerLib.hs

Manual changes:
	Moved maskedReturn isPortageBuild check into Commands.hs

Changelog:
----------------------------------------------------------------
Christian Nassif-Haynes (2):
      Show info about `set -e` suppression during function calls
      Add extra checks for masked return codes

Fabian Wolff (1):
      Do not suggest `grep -c` as a replacement for `grep -l/-L | wc -l`

Jens Petersen (1):
      move readme to extra-doc-files and add changelog to releases

Kamil Cukrowski (1):
      Add a comma to function characters

Matthias Diener (1):
      Clarify 'which'

Rebecca Cran (1):
      Fix typo in SC2006 message: "backticked" vs "backticks"

Vidar Holen (81):
      Merge pull request koalaman#2181 from matthiasdiener/patch-1
      Make x-comparison warning default
      Stable version v0.7.2
      Post-release CHANGELOG update
      Update Cabal version for Hackage
      Add wait between GitHub and Docker to allow replication
      Fix haddock failures (fixes koalaman#2216)
      Treat ${arr[*]} like $* for SC2048
      Fix bad warning for ${#arr[*]}. Fixes koalaman#2218.
      Sanity check command names (fixes koalaman#2227)
      Merge pull request koalaman#2241 from Kamilcuk/master
      Merge pull request koalaman#2238 from bcran/legacy-backticks-msg
      Merge pull request koalaman#2234 from juhp/patch-1
      SC2181: Add '!' in suggestion as appropriate (fixes koalaman#2189)
      Add :/. to chars recognized for \alias suppression (fixes koalaman#2287)
      Don't warn when line starts with &> (fixes koalaman#2281)
      Re-add warnings about 'declare var = value' (fixes koalaman#2279)
      Don't warn about repeated range in [[ -v arr[xxx] ]] (fixes koalaman#2285)
      Don't print colors when $TERM is 'dumb' or unset (fixes koalaman#2260)
      Have SC2155 trigger on 'typeset' as well (fixes koalaman#2262)
      Warn about quoting in assignments to sh declaration utilities (fixes koalaman#1556)
      Fix broken test from previous commit
      Warn about unquoted blanks in echo (fixes koalaman#377)
      Allow printf/return/assignments after exec (fixes koalaman#2249)
      Don't consider [ -n/-z/-v $var ] assignments for subshell modification (fixes koalaman#2217)
      Optionally suggest [[ over [ in Bash scripts (-o require-double-brackets) (fixes koalaman#887)
      Avoid trigger SC2181 on composite $? checks (fixes koalaman#1167)
      Warn about eval'ing arrays
      Merge pull request koalaman#2289 from nafigator/master
      SC2295 Warn about unquoted variables in PE patterns (fixes koalaman#2290)
      Switch build status badge from TravisCI to GitHub
      Remove defunct SonarQube plugin link (fixes koalaman#2292)
      Extend warnings about spaces around = to 'let'
      Suppress SC2167 when name is "_" (fixes koalaman#2298)
      Improve warnings for bad parameter expansion (fixes koalaman#2297)
      Warn about looping over array values and using them as keys
      Don't warn about variables guarded with :+ (fixes koalaman#2296)
      Recognize wait -p as assigning a variable (fixes koalaman#2179)
      Improve warnings for expr (fixes koalaman#2033)
      Add `rg` to list of commands ignored for SC2016 (fixes koalaman#2209)
      Don't warn about unused variables starting with _ (fixes koalaman#1498)
      Merge pull request koalaman#2307 from a1346054/fixes
      Fix parsing of [$var] (fixes koalaman#2309)
      Allow running this repo as a pre-commit hook
      Revert "Allow running this repo as a pre-commit hook"
      Add pre-commit instructions
      Add shellcheck-precommit hook to README.md
      Improve warnings about unnecessary subshells (fixes koalaman#2169)
      Warn about strings for numerical operators in [[ ]] (fixes koalaman#2312)
      Merge pull request koalaman#2303 from DoxasticFox/set-e-functions
      Allow specifying external-sources=true in shellcheckrc (fixes koalaman#1818)
      Merge pull request koalaman#2318 from FabianWolff/grep-lL-wc-l
      Allow `disable=all` to disable all warnings (fixes koalaman#2323)
      Remove SC1004 (fixes koalaman#2326)
      Suppress SC2094 when both are input redirections (fixes koalaman#2325)
      Don't trigger SC2140 on ${x+"a" "b"} (fixes koalaman#2265)
      Strip lines containing "STRIP" from ./striptests
      Add a `setgitversion` script to update the version string with git
      The removed check was SC1004, not SC1003
      Disable UUOC for cat with unquoted variable (fixes koalaman#2333)
      Don't emit SC2140 when trapped string is /, = or : (fixes koalaman#2334)
      Merge pull request koalaman#2320 from DoxasticFox/set-e-proc-sub
      Mention check-extra-masked-returns in changelog
      Add suggestion level in text for TTY output (fixes koalaman#2339)
      Mention require-double-brackets in CHANGELOG
      Warn about `read foo[i]` expanding as glob (fixes koalaman#2345)
      For `while getopts; do case ..` checks, make sure variable matches
      Skip SC2214 if variable is modified in loop (fixes koalaman#2351)
      Mention known incompatibilities in man page
      Treat typeset similar to declare (fixes koalaman#2354)
      Give more examples of what ShellCheck looks for
      Have quickscripts search for relevant paths (fixes koalaman#2286)
      Warn about [^..] in Dash (fixes koalaman#2361)
      Consider all forms of TA_Assignment to remove spaces (fixes koalaman#2364)
      Include `local -r` in check-extra-masked-returns (fixes koalaman#2362)
      Update release checklist
      Update distro tests
      Update stack resolver
      Update copyright years
      Fix bad version on stable releases
      Stable version 0.8.0

Yancharuk Alexander (2):
      Minor changes in README
      Review fixes in README

a1346054 (2):
      Fix redirect in LICENSE file
      Remove trailing whitespace

 .github/workflows/build.yml            |  27 +-
 .github_deploy                         |   1 -
 CHANGELOG.md                           |  51 +-
 LICENSE                                |   2 +-
 README.md                              |  38 +-
 ShellCheck.cabal                       |  12 +-
 quickrun                               |  10 +-
 quicktest                              |  11 +-
 setgitversion                          |  11 +
 shellcheck.1.md                        |  50 +-
 shellcheck.hs                          |  24 +-
 snap/snapcraft.yaml                    |   4 +-
 src/ShellCheck/AST.hs                  |   1 +
 src/ShellCheck/ASTLib.hs               |  74 ++-
 src/ShellCheck/Analytics.hs            | 841 ++++++++++++++++++++++++++++-----
 src/ShellCheck/AnalyzerLib.hs          | 182 +++++--
 src/ShellCheck/Checker.hs              |  84 +++-
 src/ShellCheck/Checks/Commands.hs      | 271 ++++++++++-
 src/ShellCheck/Checks/ShellSupport.hs  |   6 +-
 src/ShellCheck/Data.hs                 |   4 +-
 src/ShellCheck/Formatter/CheckStyle.hs |   2 +-
 src/ShellCheck/Formatter/Diff.hs       |   5 +-
 src/ShellCheck/Formatter/Format.hs     |  21 +-
 src/ShellCheck/Formatter/GCC.hs        |   2 +-
 src/ShellCheck/Formatter/JSON1.hs      |   2 +-
 src/ShellCheck/Formatter/TTY.hs        |   4 +-
 src/ShellCheck/Interface.hs            |  14 +-
 src/ShellCheck/Parser.hs               |  76 ++-
 stack.yaml                             |   2 +-
 striptests                             |   2 +-
 test/buildtest                         |   2 +
 test/check_release                     |   8 +-
 test/distrotest                        |  10 +-
 test/stacktest                         |   5 +-
 34 files changed, 1553 insertions(+), 306 deletions(-)
 create mode 100755 setgitversion

BUG=b:259131253
TEST=stack test; Compare cros lint checks on ebuilds

Change-Id: I5ca3fb27faa59b2f11369c2a150a419dee289977
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants