-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Show info about set -e
suppression during function calls
#2303
Conversation
Given that it's very idiomatic to have functions that are meant to be called in conditionals even in scripts where |
I ran this on the monorepo at work and generated 318 new shellcheck notices! Most of them look useful to me, but there's some useless ones too. I'd say the most common useless class of error is for library functions which are clearly safe. It'd be nice if we could add a shellcheck directive to the function to mark it as being safe for use in There's also a few instances where function invocations end in There's also a few cases I missed. |
Those always inherit
|
@koalaman I think I'm done making changes. PTAL when you're ready. There's a few things I'd like to talk about, centred around the phrasing of the new check (SC2304). I've noticed that, unlike this new check, the vast majority of existing ones don't say what's wrong with a code snippet. Rather, they say how it can be improved. I didn't do that, because I think there's a lot of ambiguity around the best course of action. All the cases covered by SC2304 can be fixed by turning the affected functions into scripts, but I imagine that many people would consider that excessive. We can provide more nuanced feedback for the cases where the Bash programmer can simply re-enable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
While the end result is similar, there's two very different root causes at play with different fixes:
- errexit is disabled because the command is used as a condition
- errexit is disabled because Bash has some special behavior related to certain subshells
I think it's worth separating these into two different warnings, for both message and wiki purposes:
- "This function is invoked as a condition so
set -e
will be disabled. Invoke separately if failures should cause the script to exit." - "Bash implicitly disabled
set -e
for this invocation. Addset -e;
before it or enable inherit_errexit."
This might also simplify the logic a bit as mentioned in one of the comments
src/ShellCheck/AnalyzerLib.hs
Outdated
@@ -196,7 +198,7 @@ makeParameters spec = | |||
Dash -> False | |||
Sh -> False | |||
Ksh -> True, | |||
|
|||
hasInheritErrexit = containsInheritErrexit root, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
inherit_errexit
is Bash specific. Dash and Sh already behave this way. Ksh does not support it afaik. You could set it per shell like hasLastPipe above
src/ShellCheck/Analytics.hs
Outdated
@@ -343,6 +350,31 @@ isCondition (child:parent:rest) = | |||
T_UntilExpression id c l -> take 1 . reverse $ c | |||
_ -> [] | |||
|
|||
isSetESuppressed _ [] = False | |||
isSetESuppressed _ [_] = False | |||
isSetESuppressed hasInheritErrexit (child:parent:rest) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing this won't be used anywhere, so you could move it under checkDisabledSetEInIf
src/ShellCheck/Analytics.hs
Outdated
prop_checkDisabledSetEInIf19 = verifyNotTree checkDisabledSetEInIf "set -e; f(){:;}; g(){:;}; g f" | ||
prop_checkDisabledSetEInIf20 = verifyNotTree checkDisabledSetEInIf "set -e; shopt -s inherit_errexit; f(){:;}; x=$(f)" | ||
checkDisabledSetEInIf params t = | ||
runNodeAnalysis checkNode params t |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With if hasSetE params then runNodeAnalysis checkNode params t else []
you could skip this entire check when set -e
is not in effect, instead of iterating through the nodes and checking if it's set for each.
src/ShellCheck/Analytics.hs
Outdated
checkCmd cmd = sequence_ $ do | ||
literalArg <- getUnquotedLiteral cmd | ||
Map.lookup literalArg functions_ | ||
return $ info (getId cmd) 2304 "This function invocation will run with set -e disabled." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be something like
if isCondition $ getPath (parentMap params) cmd
then return $ info ... "This function is invoked as a condition so ..."
else
if not hasInheritErrExit && isPlaceWhereBashDisablesSetE
then return $ info ... "Bash implicitly disabled `set -e` for this invocation"
else Nothing
fi
This way, it would only start inspecting the context when it finds a function, instead of doing that work for every cat
and ls
that won't lead anywhere, as well as avoid reimplementing most of the logic of the existing function isCondition
(which could be probably be updated to count T_Banged with no adverse effects)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think using isCondition
would work properly for conditions which execute multiple functions in the conditional part. For example, in the following, the f1
and f2
functions would both run without set -e
, but isCondition
would only return true for f2
:
if
f1
f2
then
:
fi
Also, now that we're entertaining the idea of splitting the warnings, something else which occurred to me is that set -e
can be disabled for multiple reasons at the same time. Examples:
x=$(foo && echo bar)
cat <(
(
x=$(foo)
echo "$x"
) || true"
)
Now I'm leaning towards giving a different error message for each case listed in getSuppressedChildren
. I'll give the ones which share the same behaviour (i.e. conditions, command substitutions and process substitutions) the same SC2xxx code, but any language construct (e.g. if
, while
, &&
, etc) which can be responsible for an error will be given its own message. Does that sound okay?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your efficiency suggestion SGTM though. Thanks!
src/ShellCheck/Analytics.hs
Outdated
T_IfExpression _ conditions _ -> concatMap fst conditions | ||
T_UntilExpression _ conditions _ -> conditions | ||
T_WhileExpression _ conditions _ -> conditions | ||
T_ProcSub _ _ cmds -> cmds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also one of the Bash specific set -e
re-enablable ones apparently
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually just realised that the behaviour of this is different than in the other cases. In the other cases, Bash disables set -e
globally, causing the function and script to continue if a command within them fail.
But the function called in a T_ProcSub
inherits set -e
, even without inherit_errexit
enabled. So the function might terminate early, as it would with set -e
. But if that happens, it'll only cause the process spawned by the T_ProcSub
to exit early, not the parent script! D: More special cases than my brain has room for!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, at least that's what happens on my machine. bash --version
gives me: GNU bash, version 5.0.3(1)-release (x86_64-pc-linux-gnu)
.
@koalaman I just implemented all the requested changes except for these (1, 2), as described in the comments. PTAL when you have time. Now when I run #!/bin/bash
set -e
bar() { :; }
if
bar && true
echo
then
:
fi
cat <(
(
x=$(bar)
echo "$x"
) || true
)
...I get this output:
|
src/ShellCheck/Analytics.hs
Outdated
-- inherits `set -e`, but if that function fails, the script | ||
-- doesn't exit. | ||
T_ProcSub _ _ cmds -> | ||
Just (cmds, sc2305) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You were right that these do inherit set -e
. The reason why it doesn't exit the script is because it runs in a subshell whose exit code is ignored. This is true for a lot of other cases as well, like f &
, echo "$(set -e; f)"
and f | cat
.
Warning about such things is not necessarily a bad feature, but it's an entirely different issue. You can just ignore this case here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point. It's not set -e
being suppressed, but the exit code.
I'd still like to raise a separate PR for the process substitution case, later on. I should give it an SCxxxx number separate from the cases you mention, right? After all, SC2155 already exists for one related case.
getAlias arg = | ||
let string = onlyLiteralString arg | ||
in when ('=' `elem` string) $ | ||
modify ((takeWhile (/= '=') string, getId arg):) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be more correct/expected to treat an alias invocation as a single command because that's almost universally what they are. This would happen if you just ignore aliases.
Sure it would basically mean copy-pasting findFunctions
, but A. they're small, B. they do slightly different things (trying to find shell-only definitions vs find functions), and C. you could optionally add (name, containsSetE t)
as the value to easily and efficiently avoid triggering on f() { set -e; false; echo foo; }; x=$(f);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ignoring aliases SGTM, but I didn't do the optional bit because I worry it'll be brittle.
src/ShellCheck/Analytics.hs
Outdated
where | ||
go [] = [] | ||
go [_] = [] | ||
go (child:parent:rest) = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm reading this right, this might all be easier as:
checkCmd = go . getPath (parentMap params)
where
go (child:parent:rest) = do
case parent of
T_AndIf _ condition _ | child `isIn` condition -> informConditional "an && condition" t
T_DollarExpansion {} | not $ errExitEnabled parent -> informUninherited t
[...]
_ -> return ()
go (parent:rest)
go _ -> return ()
errExitEnabled t = hasInheritErrexit params || containsSetE t
isIn t cmds = getId t `elem` map getId cmds
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks!
if hasSetE params then runNodeAnalysis checkNode params t else [] | ||
where | ||
checkNode _ (T_SimpleCommand _ _ (cmd:_)) = when (isFunction cmd) (checkCmd cmd) | ||
checkNode _ _ = return () |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think this warning should trigger for things like { a; b; } || c
and $(a; b)
for the same reason why it triggers for f() { a; b; }; f || c
and $(f)
?
It's just a thought and I don't expect it of this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I ran this PR on the monorepo at work, I found that set -e
was mostly suppressed in functions which were used in command substitutions and if
statements, not ||
or &&
conditions. (I think that's because people don't want to branch with &&
and ||
in production code.) So, admittedly, I don't have much empirical evidence to suggest it'll be that useful.
I also think set -e
's behaviour is more insidious for functions than the cases you mention, because, one might reasonably expect functions and commands to respond similarly to set -e
. i.e. You wouldn't expect the behaviour of the command/function to change, you'd expect the script's response to the command/function's exit code to change.
Nonetheless, I still think the cases you mention are unintuitive enough to warrant their own warnings, because nobody could know errexit
is disabled in those contexts without being told. I don't really want to raise a PR though because I haven't seen those pop up much in the wild and I'm lazy 😂
Looks good to me! Please squash into one neat commit for merge. The checks are helpful for all shells, but the optional description is bash specific. I'll probably switch it out for something broader that also highlights the problem, maybe:
I might potentially also add support for using |
Thanks, @koalaman! In addition to squashing the current commits, I made two more changes:
You read my mind! (Edit: No, wait, I mentioned that here, a fortnight ago.) Our codebase at work has some functions which are obviously safe to use with It'd still like to raise a PR for #2303 (comment) though, because it seems pretty simple and I think it's a common mistake. |
Nice! Thanks @DoxasticFox and @koalaman for the quick review! |
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
This PR provides an enhancement very similar to the one described in #2054. The following example is copied from that issue:
The current version of
shellcheck
provides no notice, although this PR produces the following:It also handles all the other cases (that I know of) where
set -e
can be disabled during a function call. The new notice can be suppressed by insertingset -e
into the correct context. For example this program produces an error inshellcheck
:But this one doesn't:
Note that Bash doesn't allow
set -e
to be "re-enabled" in most contexts. For example, this wouldn't re-enableset -e
:As a result,
shellcheck
would still display the error. Another interesting edge case is this:Although you can ordinarily re-enable
set -e
in command substitutions, the|| true
overrides that. So ashellcheck
notice is still produced in this case.It's also worth highlighting that this PR only deals provides notices about function invocations, so
shellcheck
wouldn't produce any output for the programset -e; x=$(git)
. Related issues and PRs exist (#1484, #2237) to address more general cases.