From 94214ee725122b91374b1782e93ae239aff04762 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Thu, 7 Mar 2024 19:11:12 -0800 Subject: [PATCH 01/74] Post-release CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c8beeb..b40245a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## Git +### Added +### Fixed + + ## v0.10.0 - 2024-03-07 ### Added - Precompiled binaries for macOS ARM64 (darwin.aarch64) From 50db9a29c45f1f2a0db7ec60c8850c99e8e31d6e Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Thu, 7 Mar 2024 19:11:32 -0800 Subject: [PATCH 02/74] Check source details before git details --- test/check_release | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/check_release b/test/check_release index fd1dbca..4aef69a 100755 --- a/test/check_release +++ b/test/check_release @@ -12,6 +12,17 @@ then fail "There are uncommitted changes" fi +version=${current#v} +if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version" +then + fail "The cabal file does not match tag version $version" +fi + +if ! grep -qF "## $current" CHANGELOG.md +then + fail "CHANGELOG.md does not contain '## $current'" +fi + current=$(git tag --points-at) if [[ -z "$current" ]] then @@ -34,17 +45,6 @@ then fail "You are not on master" fi -version=${current#v} -if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version" -then - fail "The cabal file does not match tag version $version" -fi - -if ! grep -qF "## $current" CHANGELOG.md -then - fail "CHANGELOG.md does not contain '## $current'" -fi - if [[ $(git log -1 --pretty=%B) != "Stable version "* ]] then fail "Expected git log message to be 'Stable version ...'" From 9cb21c8557cb981e5f49da20af9335bb68f04dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lawrence=20Vel=C3=A1zquez?= Date: Fri, 8 Mar 2024 18:24:08 -0500 Subject: [PATCH 03/74] Recommend `typeset` instead of `declare` in SC2324 Bash has both `typeset` and `declare`, but ksh has `typeset` only. Recommend the more portable alternative to users. --- src/ShellCheck/Analytics.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 1cc8bf8..f37ac1d 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -5017,7 +5017,8 @@ checkPlusEqualsNumber params t = state <- CF.getIncomingState cfga id guard $ isNumber state word guard . not $ fromMaybe False $ CF.variableMayBeDeclaredInteger state var - return $ warn id 2324 "var+=1 will append, not increment. Use (( var += 1 )), declare -i var, or quote number to silence." + -- Recommend "typeset" because ksh does not have "declare". + return $ warn id 2324 "var+=1 will append, not increment. Use (( var += 1 )), typeset -i var, or quote number to silence." _ -> return () where From 52dc66349b0882b0d6ac3d81a81f3eef0b155158 Mon Sep 17 00:00:00 2001 From: Joachim Ansorg Date: Tue, 12 Mar 2024 17:36:20 +0100 Subject: [PATCH 04/74] fix build of linux.aarch64 --- build/linux.aarch64/Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build/linux.aarch64/Dockerfile b/build/linux.aarch64/Dockerfile index d5320e9..fadb6a4 100644 --- a/build/linux.aarch64/Dockerfile +++ b/build/linux.aarch64/Dockerfile @@ -12,11 +12,15 @@ RUN apt-get update && apt-get install -y llvm gcc-$TARGET # The rest are from 22.10 RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list +# Kinetic does not receive updates anymore, switch to last available +RUN sed -e 's/archive.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list +RUN sed -e 's/security.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list + RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static # Build GHC WORKDIR /ghc -RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1 +RUN curl -L "https://downloads.haskell.org/~ghc/9.2.8/ghc-9.2.8-src.tar.xz" | tar xJ --strip-components=1 RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET" RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)" RUN make install From c4123375e04931b3d0deb08728490687bc2fb3fd Mon Sep 17 00:00:00 2001 From: Joachim Ansorg Date: Tue, 12 Mar 2024 18:00:36 +0100 Subject: [PATCH 05/74] build smaller ShellCheck binary for Linux x86_64 --- build/linux.x86_64/Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build/linux.x86_64/Dockerfile b/build/linux.x86_64/Dockerfile index 3112ac2..edafb36 100644 --- a/build/linux.x86_64/Dockerfile +++ b/build/linux.x86_64/Dockerfile @@ -1,4 +1,8 @@ -FROM alpine:latest +FROM alpine:3.16 +# alpine:3.16 (GHC 9.0.1): 5.8 megabytes +# alpine:3.17 (GHC 9.0.2): 15.0 megabytes +# alpine:3.18 (GHC 9.4.4): 29.0 megabytes +# alpine:3.19 (GHC 9.4.7): 29.0 megabytes ENV TARGETNAME linux.x86_64 From 0a7bb1822e9c80dae21aa6aaba6d8da3de5d9e94 Mon Sep 17 00:00:00 2001 From: Hugo Sousa <55895340+hugos99@users.noreply.github.com> Date: Thu, 4 Apr 2024 12:26:20 +0100 Subject: [PATCH 06/74] Update README.md to add macOS Arm64 pre-compiled binaries link --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6542ac2..366c427 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,7 @@ Alternatively, you can download pre-compiled binaries for the latest release her * [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked) * [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked) * [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked) +* [macOS, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.aarch64.tar.xz) * [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz) * [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip) From 30b32af873c8b9e24731a5cb08bb20b7f148fa2d Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Thu, 4 Apr 2024 19:50:08 -0700 Subject: [PATCH 07/74] Add updating build images to release checks --- test/check_release | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/check_release b/test/check_release index 4aef69a..665b265 100755 --- a/test/check_release +++ b/test/check_release @@ -56,11 +56,14 @@ cat << EOF Manual Checklist $((i++)). Make sure none of the automated checks above failed +$((i++)). Run \`build/build_builder build/*/\` to update all builder images. +$((j++)). \`build/run_builder dist-newstyle/sdist/ShellCheck-*.tar.gz build/*/\` to verify that they work. +$((j++)). \`for f in \$(cat build/*/tag); do docker push "\$f"; done\` to upload them. +$((i++)). Run test/distrotest to ensure that most distros can build OOTB. $((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions $((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman -$((i++)). Run test/distrotest to ensure that most distros can build OOTB. $((i++)). Format and read over the manual for bad formatting and outdated info. -$((i++)). Make sure the Hackage package builds. +$((i++)). Make sure the Hackage package builds locally. Release Steps From 5241878e5919d3581a8e0208c3d2345532dbb65f Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Fri, 5 Apr 2024 17:15:04 -0700 Subject: [PATCH 08/74] Update Windows build image with new cURL URL --- build/windows.x86_64/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/windows.x86_64/Dockerfile b/build/windows.x86_64/Dockerfile index 1e5c5d9..2ae78ac 100644 --- a/build/windows.x86_64/Dockerfile +++ b/build/windows.x86_64/Dockerfile @@ -12,7 +12,7 @@ WORKDIR /haskell RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1 WORKDIR /haskell/bin RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip - -RUN curl -L "https://curl.se/windows/dl-7.84.0/curl-7.84.0-win64-mingw.zip" | busybox unzip - && mv curl-7.84.0-win64-mingw/bin/* . +RUN curl -L "https://curl.se/windows/dl-8.7.1_7/curl-8.7.1_7-win64-mingw.zip" | busybox unzip - && mv curl-*-win64-mingw/bin/* . ENV WINEPATH /haskell/bin # It's unknown whether Cabal on Windows suffers from the same issue From 04a86245a10fe0a9e48755237f315999986f54c0 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Mon, 8 Apr 2024 20:24:28 -0700 Subject: [PATCH 09/74] Remove trailing space in output (fixes #2961) --- src/ShellCheck/Formatter/TTY.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ShellCheck/Formatter/TTY.hs b/src/ShellCheck/Formatter/TTY.hs index e503639..117da6e 100644 --- a/src/ShellCheck/Formatter/TTY.hs +++ b/src/ShellCheck/Formatter/TTY.hs @@ -169,7 +169,7 @@ showFixedString color comments lineNum fileLines = -- and/or other unrelated lines. let (excerptFix, excerpt) = sliceFile mergedFix fileLines -- in the spirit of error prone - putStrLn $ color "message" "Did you mean: " + putStrLn $ color "message" "Did you mean:" putStrLn $ unlines $ applyFix excerptFix excerpt cuteIndent :: PositionedComment -> String From 2c5155e43d030e1325c3c2765d8f492024b02fd9 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 14 Apr 2024 18:47:19 -0700 Subject: [PATCH 10/74] Warn about capturing the output of redirected commands. --- CHANGELOG.md | 1 + src/ShellCheck/Analytics.hs | 42 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b40245a..25fc688 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## Git ### Added +- SC2327/SC2328: Warn about capturing the output of redirected commands. ### Fixed diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index f37ac1d..8b74ac6 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -204,6 +204,7 @@ nodeChecks = [ ,checkUnnecessaryArithmeticExpansionIndex ,checkUnnecessaryParens ,checkPlusEqualsNumber + ,checkExpansionWithRedirection ] optionalChecks = map fst optionalTreeChecks @@ -5040,5 +5041,46 @@ checkPlusEqualsNumber params t = isUnquotedNumber || isNumericalVariableName || isNumericalVariableExpansion + +prop_checkExpansionWithRedirection1 = verify checkExpansionWithRedirection "var=$(foo > bar)" +prop_checkExpansionWithRedirection2 = verify checkExpansionWithRedirection "var=`foo 1> bar`" +prop_checkExpansionWithRedirection3 = verify checkExpansionWithRedirection "var=${ foo >> bar; }" +prop_checkExpansionWithRedirection4 = verify checkExpansionWithRedirection "var=$(foo | bar > baz)" +prop_checkExpansionWithRedirection5 = verifyNot checkExpansionWithRedirection "stderr=$(foo 2>&1 > /dev/null)" +prop_checkExpansionWithRedirection6 = verifyNot checkExpansionWithRedirection "var=$(foo; bar > baz)" +prop_checkExpansionWithRedirection7 = verifyNot checkExpansionWithRedirection "var=$(foo > bar; baz)" +prop_checkExpansionWithRedirection8 = verifyNot checkExpansionWithRedirection "var=$(cat <&3)" +checkExpansionWithRedirection params t = + case t of + T_DollarExpansion id [cmd] -> check id cmd + T_Backticked id [cmd] -> check id cmd + T_DollarBraceCommandExpansion id [cmd] -> check id cmd + _ -> return () + where + check id pipe = + case pipe of + (T_Pipeline _ _ t@(_:_)) -> checkCmd id (last t) + _ -> return () + + checkCmd captureId (T_Redirecting _ redirs _) = walk captureId redirs + + walk captureId [] = return () + walk captureId (t:rest) = + case t of + T_FdRedirect _ _ (T_IoDuplicate _ _ "1") -> return () + T_FdRedirect id "1" (T_IoDuplicate _ _ _) -> return () + T_FdRedirect id "" (T_IoDuplicate _ op _) | op `elem` [T_GREATAND (Id 0), T_Greater (Id 0)] -> emit id captureId True + T_FdRedirect id str (T_IoFile _ op file) | str `elem` ["", "1"] && op `elem` [ T_DGREAT (Id 0), T_Greater (Id 0) ] -> + if getLiteralString file == Just "/dev/null" + then emit id captureId False + else emit id captureId True + _ -> walk captureId rest + + emit redirectId captureId suggestTee = do + warn captureId 2327 "This command substitution will be empty because the command's output gets redirected away." + err redirectId 2328 $ "This redirection takes output away from the command substitution" ++ if suggestTee then " (use tee to duplicate)." else "." + + + return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) From 69fe4e1306572fa639d92e80afe2639d6c12247f Mon Sep 17 00:00:00 2001 From: Syuugo Date: Thu, 25 Apr 2024 10:35:43 +0900 Subject: [PATCH 11/74] Upgrade build workflow dependencies --- .github/workflows/build.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c49b25a..378a0cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: sudo apt-get install cabal-install - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 @@ -37,7 +37,7 @@ jobs: mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: source path: source/ @@ -51,10 +51,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 - name: Build source run: | @@ -63,7 +63,7 @@ jobs: ( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} ) - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: bin path: bin/ @@ -74,10 +74,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 - name: Work around GitHub permissions bug run: chmod +x bin/*/shellcheck* @@ -92,7 +92,7 @@ jobs: rm -rf */ README* LICENSE* - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: deploy path: deploy/ @@ -109,10 +109,10 @@ jobs: sudo apt-get install hub - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 - name: Upload to GitHub env: From 796c6bd8482e4666c4f73d2874c3949c2bed801e Mon Sep 17 00:00:00 2001 From: Jan Dubois Date: Wed, 24 Apr 2024 19:05:29 -0700 Subject: [PATCH 12/74] Add new bats variables stderr and stderr_lines These are being set by `run --separate-stderr` and have been introduced in https://github.com/bats-core/bats-core/releases/tag/v1.5.0 --- src/ShellCheck/AnalyzerLib.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ShellCheck/AnalyzerLib.hs b/src/ShellCheck/AnalyzerLib.hs index d265ace..a2d61d2 100644 --- a/src/ShellCheck/AnalyzerLib.hs +++ b/src/ShellCheck/AnalyzerLib.hs @@ -535,7 +535,9 @@ getModifiedVariables t = T_BatsTest {} -> [ (t, t, "lines", DataArray SourceExternal), (t, t, "status", DataString SourceInteger), - (t, t, "output", DataString SourceExternal) + (t, t, "output", DataString SourceExternal), + (t, t, "stderr", DataString SourceExternal), + (t, t, "stderr_lines", DataArray SourceExternal) ] -- Count [[ -v foo ]] as an "assignment". From 4f81dbe839091a06a5cfaea695cf1c451ff07565 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sat, 4 May 2024 14:21:12 -0700 Subject: [PATCH 13/74] Add warning about uninvoked functions, reduce repeated triggering of SC2317 (fixes #2966) --- CHANGELOG.md | 2 ++ src/ShellCheck/Analytics.hs | 23 ++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25fc688..5277893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ ## Git ### Added - SC2327/SC2328: Warn about capturing the output of redirected commands. +- SC2329: Warn when (non-escaping) functions are never invoked. ### Fixed +- SC2317 about unreachable commands is now less spammy for nested ones. ## v0.10.0 - 2024-03-07 diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 8b74ac6..685fbf4 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -4896,16 +4896,33 @@ checkBatsTestDoesNotUseNegation params t = prop_checkCommandIsUnreachable1 = verify checkCommandIsUnreachable "foo; bar; exit; baz" prop_checkCommandIsUnreachable2 = verify checkCommandIsUnreachable "die() { exit; }; foo; bar; die; baz" prop_checkCommandIsUnreachable3 = verifyNot checkCommandIsUnreachable "foo; bar || exit; baz" +prop_checkCommandIsUnreachable4 = verifyNot checkCommandIsUnreachable "f() { foo; }; # Maybe sourced" +prop_checkCommandIsUnreachable5 = verify checkCommandIsUnreachable "f() { foo; }; exit # Not sourced" checkCommandIsUnreachable params t = case t of T_Pipeline {} -> sequence_ $ do cfga <- cfgAnalysis params - state <- CF.getIncomingState cfga id + state <- CF.getIncomingState cfga (getId t) guard . not $ CF.stateIsReachable state guard . not $ isSourced params t - return $ info id 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)." + guard . not $ any (\t -> isUnreachable t || isUnreachableFunction t) $ NE.drop 1 $ getPath (parentMap params) t + return $ info (getId t) 2317 "Command appears to be unreachable. Check usage (or ignore if invoked indirectly)." + T_Function id _ _ _ _ -> + when (isUnreachableFunction t + && (not . any isUnreachableFunction . NE.drop 1 $ getPath (parentMap params) t) + && (not $ isSourced params t)) $ + info id 2329 "This function is never invoked. Check usage (or ignored if invoked indirectly)." _ -> return () - where id = getId t + where + isUnreachableFunction :: Token -> Bool + isUnreachableFunction f = + case f of + T_Function id _ _ _ t -> isUnreachable t + _ -> False + isUnreachable t = fromMaybe False $ do + cfga <- cfgAnalysis params + state <- CF.getIncomingState cfga (getId t) + return . not $ CF.stateIsReachable state prop_checkOverwrittenExitCode1 = verify checkOverwrittenExitCode "x; [ $? -eq 1 ] || [ $? -eq 2 ]" From 76ff702e9385215a888ed21bf4330f614ab6c355 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sat, 4 May 2024 15:12:13 -0700 Subject: [PATCH 14/74] Supress SC2015 about `A && B || C` when B is a test. --- CHANGELOG.md | 2 ++ src/ShellCheck/Analytics.hs | 16 +++++----------- src/ShellCheck/AnalyzerLib.hs | 8 ++++++++ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5277893..fe38b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Added - SC2327/SC2328: Warn about capturing the output of redirected commands. - SC2329: Warn when (non-escaping) functions are never invoked. +### Changed +- SC2015 about `A && B || C` no longer triggers when B is a test command. ### Fixed - SC2317 about unreachable commands is now less spammy for nested ones. diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 685fbf4..175dea6 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -877,8 +877,9 @@ prop_checkShorthandIf5 = verifyNot checkShorthandIf "foo && rm || printf b" prop_checkShorthandIf6 = verifyNot checkShorthandIf "if foo && bar || baz; then true; fi" prop_checkShorthandIf7 = verifyNot checkShorthandIf "while foo && bar || baz; do true; done" prop_checkShorthandIf8 = verify checkShorthandIf "if true; then foo && bar || baz; fi" -checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ _) (T_Pipeline _ _ t)) - | not (isOk t || inCondition) = +prop_checkShorthandIf9 = verifyNot checkShorthandIf "foo && [ -x /file ] || bar" +checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ b) (T_Pipeline _ _ t)) + | not (isOk t || inCondition) && not (isTestCommand b) = info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true." where isOk [t] = isAssignment t || fromMaybe False (do @@ -4197,7 +4198,7 @@ checkBadTestAndOr params t = in mapM_ checkTest commandWithSeps checkTest (before, cmd, after) = - when (isTest cmd) $ do + when (isTestCommand cmd) $ do checkPipe before checkPipe after @@ -4213,17 +4214,10 @@ checkBadTestAndOr params t = T_AndIf _ _ rhs -> checkAnds id rhs T_OrIf _ _ rhs -> checkAnds id rhs T_Pipeline _ _ list | not (null list) -> checkAnds id (last list) - cmd -> when (isTest cmd) $ + cmd -> when (isTestCommand cmd) $ errWithFix id 2265 "Use && for logical AND. Single & will background and return true." $ (fixWith [replaceEnd id params 0 "&"]) - isTest t = - case t of - T_Condition {} -> True - T_SimpleCommand {} -> t `isCommand` "test" - T_Redirecting _ _ t -> isTest t - T_Annotation _ _ t -> isTest t - _ -> False prop_checkComparisonWithLeadingX1 = verify checkComparisonWithLeadingX "[ x$foo = xlol ]" prop_checkComparisonWithLeadingX2 = verify checkComparisonWithLeadingX "test x$foo = xlol" diff --git a/src/ShellCheck/AnalyzerLib.hs b/src/ShellCheck/AnalyzerLib.hs index d265ace..c6e4e14 100644 --- a/src/ShellCheck/AnalyzerLib.hs +++ b/src/ShellCheck/AnalyzerLib.hs @@ -929,6 +929,14 @@ modifiesVariable params token name = Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name _ -> False +isTestCommand t = + case t of + T_Condition {} -> True + T_SimpleCommand {} -> t `isCommand` "test" + T_Redirecting _ _ t -> isTestCommand t + T_Annotation _ _ t -> isTestCommand t + T_Pipeline _ _ [t] -> isTestCommand t + _ -> False return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) From d705716dc45d58eef51231292758e2af9e3da30b Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sat, 4 May 2024 15:22:09 -0700 Subject: [PATCH 15/74] Account for annotations in SC2215. Fixes #2975. --- src/ShellCheck/Analytics.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 175dea6..dcb4ce8 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -4000,6 +4000,7 @@ prop_checkUselessBang6 = verify checkUselessBang "set -e; { ! true; }" prop_checkUselessBang7 = verifyNot checkUselessBang "set -e; x() { ! [ x ]; }" prop_checkUselessBang8 = verifyNot checkUselessBang "set -e; if { ! true; }; then true; fi" prop_checkUselessBang9 = verifyNot checkUselessBang "set -e; while ! true; do true; done" +prop_checkUselessBang10 = verify checkUselessBang "set -e\nshellcheck disable=SC0000\n! true\nrest" checkUselessBang params t = when (hasSetE params) $ mapM_ check (getNonReturningCommands t) where check t = @@ -4008,6 +4009,7 @@ checkUselessBang params t = when (hasSetE params) $ mapM_ check (getNonReturning addComment $ makeCommentWithFix InfoC id 2251 "This ! is not on a condition and skips errexit. Use `&& exit 1` instead, or make sure $? is checked." (fixWith [replaceStart id params 1 "", replaceEnd (getId cmd) params 0 " && exit 1"]) + T_Annotation _ _ t -> check t _ -> return () -- Get all the subcommands that aren't likely to be the return value From a7a906e2cbca41611f232208ae062fe7ddc719f7 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sat, 4 May 2024 16:28:56 -0700 Subject: [PATCH 16/74] Allow SC2154 to trigger in arrays (fixes #2970) --- src/ShellCheck/Analytics.hs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index dcb4ce8..dc84a78 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -2447,6 +2447,7 @@ prop_checkUnassignedReferences_minusZDefault = verifyNotTree checkUnassignedRefe prop_checkUnassignedReferences50 = verifyNotTree checkUnassignedReferences "echo ${foo:+bar}" prop_checkUnassignedReferences51 = verifyNotTree checkUnassignedReferences "echo ${foo:+$foo}" prop_checkUnassignedReferences52 = verifyNotTree checkUnassignedReferences "wait -p pid; echo $pid" +prop_checkUnassignedReferences53 = verify checkUnassignedReferences "x=($foo)" checkUnassignedReferences = checkUnassignedReferences' False checkUnassignedReferences' includeGlobals params t = warnings @@ -2502,14 +2503,12 @@ checkUnassignedReferences' includeGlobals params t = warnings warnings = execWriter . sequence $ mapMaybe warningFor unassigned - -- Due to parsing, foo=( [bar]=baz ) parses 'bar' as a reference even for assoc arrays. - -- Similarly, ${foo[bar baz]} may not be referencing bar/baz. Just skip these. + -- ${foo[bar baz]} may not be referencing bar/baz. Just skip these. -- We can also have ${foo:+$foo} should be treated like [[ -n $foo ]] && echo $foo isException var t = any shouldExclude $ getPath (parentMap params) t where shouldExclude t = case t of - T_Array {} -> True (T_DollarBraced _ _ l) -> let str = concat $ oversimplify l ref = getBracedReference str From a13cb85f49c074ce6ab4644beaf8665a3ff6f395 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sat, 4 May 2024 16:34:21 -0700 Subject: [PATCH 17/74] Fixed broken test due to bad build cache --- src/ShellCheck/Analytics.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index dc84a78..f5e57e2 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -2447,7 +2447,7 @@ prop_checkUnassignedReferences_minusZDefault = verifyNotTree checkUnassignedRefe prop_checkUnassignedReferences50 = verifyNotTree checkUnassignedReferences "echo ${foo:+bar}" prop_checkUnassignedReferences51 = verifyNotTree checkUnassignedReferences "echo ${foo:+$foo}" prop_checkUnassignedReferences52 = verifyNotTree checkUnassignedReferences "wait -p pid; echo $pid" -prop_checkUnassignedReferences53 = verify checkUnassignedReferences "x=($foo)" +prop_checkUnassignedReferences53 = verifyTree checkUnassignedReferences "x=($foo)" checkUnassignedReferences = checkUnassignedReferences' False checkUnassignedReferences' includeGlobals params t = warnings From ac8fb00504ed6da83fe5c5f83e72e4663ff6b439 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sat, 4 May 2024 16:45:52 -0700 Subject: [PATCH 18/74] Account for BusyBox support of [[ ]] (fixes #2967) --- CHANGELOG.md | 2 ++ src/ShellCheck/Analytics.hs | 11 ++++++++--- src/ShellCheck/AnalyzerLib.hs | 10 ---------- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe38b8a..8ae176d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,12 @@ ### Added - SC2327/SC2328: Warn about capturing the output of redirected commands. - SC2329: Warn when (non-escaping) functions are never invoked. +- SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox. ### Changed - SC2015 about `A && B || C` no longer triggers when B is a test command. ### Fixed - SC2317 about unreachable commands is now less spammy for nested ones. +- SC2292, optional suggestion for [[ ]], now triggers for Busybox. ## v0.10.0 - 2024-03-07 diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index f5e57e2..a89f940 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -1525,6 +1525,7 @@ prop_checkComparisonAgainstGlob3 = verify checkComparisonAgainstGlob "[ $cow = * prop_checkComparisonAgainstGlob4 = verifyNot checkComparisonAgainstGlob "[ $cow = foo ]" prop_checkComparisonAgainstGlob5 = verify checkComparisonAgainstGlob "[[ $cow != $bar ]]" prop_checkComparisonAgainstGlob6 = verify checkComparisonAgainstGlob "[ $f != /* ]" +prop_checkComparisonAgainstGlob7 = verify checkComparisonAgainstGlob "#!/bin/busybox sh\n[[ $f == *foo* ]]" checkComparisonAgainstGlob _ (TC_Binary _ DoubleBracket op _ (T_NormalWord id [T_DollarBraced _ _ _])) | op `elem` ["=", "==", "!="] = warn id 2053 $ "Quote the right-hand side of " ++ op ++ " in [[ ]] to prevent glob matching." @@ -1532,10 +1533,14 @@ checkComparisonAgainstGlob params (TC_Binary _ SingleBracket op _ word) | op `elem` ["=", "==", "!="] && isGlob word = err (getId word) 2081 msg where - msg = if isBashLike params + msg = if (shellType params) `elem` [Bash, Ksh] -- Busybox does not support glob matching then "[ .. ] can't match globs. Use [[ .. ]] or case statement." else "[ .. ] can't match globs. Use a case statement." +checkComparisonAgainstGlob params (TC_Binary _ DoubleBracket op _ word) + | shellType params == BusyboxSh && op `elem` ["=", "==", "!="] && isGlob word = + err (getId word) 2330 "BusyBox [[ .. ]] does not support glob matching. Use a case statement." + checkComparisonAgainstGlob _ _ = return () prop_checkCaseAgainstGlob1 = verify checkCaseAgainstGlob "case foo in lol$n) foo;; esac" @@ -4534,13 +4539,13 @@ prop_checkRequireDoubleBracket2 = verifyTree checkRequireDoubleBracket "[ foo -o prop_checkRequireDoubleBracket3 = verifyNotTree checkRequireDoubleBracket "#!/bin/sh\n[ -x foo ]" prop_checkRequireDoubleBracket4 = verifyNotTree checkRequireDoubleBracket "[[ -x foo ]]" checkRequireDoubleBracket params = - if isBashLike params + if (shellType params) `elem` [Bash, Ksh, BusyboxSh] then nodeChecksToTreeCheck [check] params else const [] where check _ t = case t of T_Condition id SingleBracket _ -> - styleWithFix id 2292 "Prefer [[ ]] over [ ] for tests in Bash/Ksh." (fixFor t) + styleWithFix id 2292 "Prefer [[ ]] over [ ] for tests in Bash/Ksh/Busybox." (fixFor t) _ -> return () fixFor t = fixWith $ diff --git a/src/ShellCheck/AnalyzerLib.hs b/src/ShellCheck/AnalyzerLib.hs index c6e4e14..cae73b1 100644 --- a/src/ShellCheck/AnalyzerLib.hs +++ b/src/ShellCheck/AnalyzerLib.hs @@ -902,16 +902,6 @@ supportsArrays Bash = True supportsArrays Ksh = True supportsArrays _ = False --- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh) -isBashLike :: Parameters -> Bool -isBashLike params = - case shellType params of - Bash -> True - Ksh -> True - Dash -> False - BusyboxSh -> False - Sh -> False - isTrueAssignmentSource c = case c of DataString SourceChecked -> False From 78d1ee0222a114597abba1ca8f0784b673bf7d97 Mon Sep 17 00:00:00 2001 From: Bryan Honof Date: Fri, 24 May 2024 17:15:09 +0200 Subject: [PATCH 19/74] Add Flox to list of installation methods --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 366c427..754f897 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,11 @@ Using the [nix package manager](https://nixos.org/nix): nix-env -iA nixpkgs.shellcheck ``` +Using the [Flox package manager](https://flox.dev/) +```sh +flox install shellcheck +``` + Alternatively, you can download pre-compiled binaries for the latest release here: * [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked) From 15de97e33f462434272f13e1ec3aa9cd5387441b Mon Sep 17 00:00:00 2001 From: Meng Zhuo Date: Thu, 30 May 2024 19:20:21 +0800 Subject: [PATCH 20/74] Add linux.riscv64 precompiled support --- .github/workflows/build.yml | 2 +- .multi_arch_docker | 1 + CHANGELOG.md | 1 + build/linux.riscv64/Dockerfile | 47 ++++++++++++++++++++++++++++++++++ build/linux.riscv64/build | 15 +++++++++++ build/linux.riscv64/tag | 1 + 6 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 build/linux.riscv64/Dockerfile create mode 100755 build/linux.riscv64/build create mode 100644 build/linux.riscv64/tag diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c49b25a..e30b2ac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,7 +47,7 @@ jobs: needs: package_source strategy: matrix: - build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, darwin.aarch64, windows.x86_64] + build: [linux.x86_64, linux.aarch64, linux.armv6hf, linux.riscv64, darwin.x86_64, darwin.aarch64, windows.x86_64] runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.multi_arch_docker b/.multi_arch_docker index 1c5d32b..81048a2 100755 --- a/.multi_arch_docker +++ b/.multi_arch_docker @@ -80,6 +80,7 @@ function multi_arch_docker::main() { export DOCKER_PLATFORMS='linux/amd64' DOCKER_PLATFORMS+=' linux/arm64' DOCKER_PLATFORMS+=' linux/arm/v6' + DOCKER_PLATFORMS+=' linux/riscv64' multi_arch_docker::install_docker_buildx multi_arch_docker::login_to_docker_hub diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae176d..43db00c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - SC2327/SC2328: Warn about capturing the output of redirected commands. - SC2329: Warn when (non-escaping) functions are never invoked. - SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox. +- Precompiled binaries for Linux riscv64 (linux.riscv64) ### Changed - SC2015 about `A && B || C` no longer triggers when B is a test command. ### Fixed diff --git a/build/linux.riscv64/Dockerfile b/build/linux.riscv64/Dockerfile new file mode 100644 index 0000000..648ef2d --- /dev/null +++ b/build/linux.riscv64/Dockerfile @@ -0,0 +1,47 @@ +FROM ubuntu:22.04 + +ENV TARGETNAME linux.riscv64 +ENV TARGET riscv64-linux-gnu + +USER root +ENV DEBIAN_FRONTEND noninteractive + +# Init base +RUN apt update -y + +# Install qemu +RUN apt install -y --no-install-recommends build-essential ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev curl ca-certificates python3-virtualenv +WORKDIR /qemu +RUN curl -Lv "https://download.qemu.org/qemu-9.0.0.tar.xz" | tar xJ --strip-components=1 +RUN ./configure --target-list=riscv64-linux-user --static --disable-system --disable-pie +RUN cd build && ninja qemu-riscv64 +ENV QEMU_EXECVE 1 + + +# Set up a riscv64 userspace +RUN apt install -y --no-install-recommends debootstrap +RUN debootstrap --arch=riscv64 --foreign jammy /rvfs http://ports.ubuntu.com/ubuntu-ports +RUN cp /qemu/build/qemu-riscv64 /rvfs/usr/bin/qemu-riscv64-static + +RUN printf > /bin/rv '%s\n' '#!/bin/sh' 'chroot /rvfs /usr/bin/qemu-riscv64-static /usr/bin/env "$@"' +RUN chmod +x /bin/rv +RUN [ ! -e /rvfs/debootstrap ] || rv '/debootstrap/debootstrap' --second-stage + +# Install deps in the chroot +RUN printf > /rvfs/etc/apt/sources.list '%s\n' 'deb http://ports.ubuntu.com/ubuntu-ports jammy main universe' +RUN rv apt update -y +RUN rv apt install -y --no-install-recommends ghc cabal-install + +# Finally we can build the current dependencies. This takes hours. +# jobs must be 1, GHS riscv will use about 40G memory +RUN rv cabal update +RUN IFS=";" && rv cabal install --dependencies-only --jobs=1 ShellCheck +RUN IFS=';' && rv cabal install --lib --jobs=1 fgl + +# Clean up +RUN rm -rf /qemu + +# Copy the build script +WORKDIR /rvfs/scratch +COPY build /rvfs/usr/bin/build +ENTRYPOINT ["/bin/rv", "/usr/bin/build"] diff --git a/build/linux.riscv64/build b/build/linux.riscv64/build new file mode 100755 index 0000000..19cb143 --- /dev/null +++ b/build/linux.riscv64/build @@ -0,0 +1,15 @@ +#!/bin/sh +set -xe +{ + tar xzv --strip-components=1 + chmod +x striptests && ./striptests + mkdir "$TARGETNAME" + cabal update + ( IFS=';'; cabal build --enable-executable-static ) + find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; + ls -l "$TARGETNAME" + "$TARGET-strip" -s "$TARGETNAME/shellcheck" + ls -l "$TARGETNAME" + qemu-riscv64-static "$TARGETNAME/shellcheck" --version +} >&2 +tar czv "$TARGETNAME" diff --git a/build/linux.riscv64/tag b/build/linux.riscv64/tag new file mode 100644 index 0000000..901eaaa --- /dev/null +++ b/build/linux.riscv64/tag @@ -0,0 +1 @@ +koalaman/scbuilder-linux-riscv64 From 23e76de4f2a640ecd7c8c1da23495d985ef095e6 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Tue, 11 Jun 2024 06:00:22 +0000 Subject: [PATCH 21/74] Allow riscv64 image to run without binfmt_misc --- build/linux.riscv64/Dockerfile | 44 ++++++----- build/linux.riscv64/build | 10 ++- build/linux.riscv64/cabal.project.freeze | 93 ++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 21 deletions(-) create mode 100644 build/linux.riscv64/cabal.project.freeze diff --git a/build/linux.riscv64/Dockerfile b/build/linux.riscv64/Dockerfile index 648ef2d..0ac95e5 100644 --- a/build/linux.riscv64/Dockerfile +++ b/build/linux.riscv64/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:22.04 +FROM ubuntu:24.04 ENV TARGETNAME linux.riscv64 ENV TARGET riscv64-linux-gnu @@ -7,39 +7,47 @@ USER root ENV DEBIAN_FRONTEND noninteractive # Init base -RUN apt update -y +RUN apt-get update -y # Install qemu -RUN apt install -y --no-install-recommends build-essential ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev curl ca-certificates python3-virtualenv +RUN apt-get install -y --no-install-recommends build-essential ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev curl ca-certificates python3-virtualenv git python3-setuptools WORKDIR /qemu -RUN curl -Lv "https://download.qemu.org/qemu-9.0.0.tar.xz" | tar xJ --strip-components=1 -RUN ./configure --target-list=riscv64-linux-user --static --disable-system --disable-pie +RUN git clone --depth 1 https://github.com/koalaman/qemu . +#RUN git clone https://github.com/balena-io/qemu . +# Release 7.0.0 +#RUN git checkout 639d1d8903f65d74eb04c49e0df7a4b2f014cd86 +RUN ./configure --target-list=riscv64-linux-user --static --disable-system --disable-pie --disable-werror RUN cd build && ninja qemu-riscv64 ENV QEMU_EXECVE 1 # Set up a riscv64 userspace -RUN apt install -y --no-install-recommends debootstrap -RUN debootstrap --arch=riscv64 --foreign jammy /rvfs http://ports.ubuntu.com/ubuntu-ports +RUN apt-get install -y --no-install-recommends debootstrap +RUN debootstrap --arch=riscv64 --foreign noble /rvfs http://ports.ubuntu.com/ubuntu-ports RUN cp /qemu/build/qemu-riscv64 /rvfs/usr/bin/qemu-riscv64-static -RUN printf > /bin/rv '%s\n' '#!/bin/sh' 'chroot /rvfs /usr/bin/qemu-riscv64-static /usr/bin/env "$@"' +# Command to run riscv binaries in the chroot. The Haskell runtime allocates 1TB +# vspace up front and QEmu has a RAM cost per vspace, so use ulimit to allocate +# less and reduce RAM usage. +RUN printf > /bin/rv '%s\n' '#!/bin/sh' 'ulimit -v $((10*1024*1024)); chroot /rvfs /usr/bin/qemu-riscv64-static /usr/bin/env "$@"' RUN chmod +x /bin/rv RUN [ ! -e /rvfs/debootstrap ] || rv '/debootstrap/debootstrap' --second-stage # Install deps in the chroot -RUN printf > /rvfs/etc/apt/sources.list '%s\n' 'deb http://ports.ubuntu.com/ubuntu-ports jammy main universe' -RUN rv apt update -y -RUN rv apt install -y --no-install-recommends ghc cabal-install +RUN printf > /rvfs/etc/apt/sources.list '%s\n' 'deb http://ports.ubuntu.com/ubuntu-ports noble main universe' +RUN rv apt-get update -y +RUN rv apt-get install -y --no-install-recommends ghc cabal-install -# Finally we can build the current dependencies. This takes hours. -# jobs must be 1, GHS riscv will use about 40G memory RUN rv cabal update -RUN IFS=";" && rv cabal install --dependencies-only --jobs=1 ShellCheck -RUN IFS=';' && rv cabal install --lib --jobs=1 fgl - -# Clean up -RUN rm -rf /qemu +# Generated with: cabal freeze -c 'hashable -arch-native'. We put it in /etc so cabal won't find it. +COPY cabal.project.freeze /rvfs/etc +# Awful hack to install everything from the freeze file +# This basically turns 'any.tagged ==0.8.8' into tagged-0.8.8 to install by version, +# and adds a -c before 'hashable -arch-native +integer-gmp' to make it a flag constraint. +RUN < /rvfs/etc/cabal.project.freeze sed 's/constraints:/&\n /' | grep -vw rts | sed -n -e 's/^ *\([^,]*\).*/\1/p' | sed -e 's/any\.\([^ ]*\) ==\(.*\)/\1-\2/; te; s/.*/-c\n&/; :e' > /tmp/preinstall-flags +# Finally we can build the current dependencies. This takes hours. +# There's apparently a random segfault during assembly, so retry a few times. +RUN set -x; IFS=${IFS# }; f() { rv cabal install --keep-going $(cat /tmp/preinstall-flags); }; for i in $(seq 5); do f; ret=$?; [ $ret = 0 ] && break; done; exit $ret # Copy the build script WORKDIR /rvfs/scratch diff --git a/build/linux.riscv64/build b/build/linux.riscv64/build index 19cb143..63c487f 100755 --- a/build/linux.riscv64/build +++ b/build/linux.riscv64/build @@ -1,15 +1,19 @@ #!/bin/sh set -xe +IFS=';' { tar xzv --strip-components=1 chmod +x striptests && ./striptests + # Use a freeze file to ensure we use the same dependencies we cached during + # the docker image build. We don't want to spend time compiling anything new. + cp /etc/cabal.project.freeze . mkdir "$TARGETNAME" - cabal update - ( IFS=';'; cabal build --enable-executable-static ) + # Retry in case of random segfault + cabal build --enable-executable-static || cabal build --enable-executable-static find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; ls -l "$TARGETNAME" "$TARGET-strip" -s "$TARGETNAME/shellcheck" ls -l "$TARGETNAME" - qemu-riscv64-static "$TARGETNAME/shellcheck" --version + "$TARGETNAME/shellcheck" --version } >&2 tar czv "$TARGETNAME" diff --git a/build/linux.riscv64/cabal.project.freeze b/build/linux.riscv64/cabal.project.freeze new file mode 100644 index 0000000..cbb42e1 --- /dev/null +++ b/build/linux.riscv64/cabal.project.freeze @@ -0,0 +1,93 @@ +active-repositories: hackage.haskell.org:merge +constraints: any.Diff ==0.5, + any.OneTuple ==0.4.2, + any.QuickCheck ==2.14.3, + QuickCheck -old-random +templatehaskell, + any.StateVar ==1.2.2, + any.aeson ==2.2.3.0, + aeson +ordered-keymap, + any.array ==0.5.4.0, + any.assoc ==1.1.1, + assoc -tagged, + any.base ==4.17.2.0, + any.base-orphans ==0.9.2, + any.bifunctors ==5.6.2, + bifunctors +tagged, + any.binary ==0.8.9.1, + any.bytestring ==0.11.5.2, + any.character-ps ==0.1, + any.comonad ==5.0.8, + comonad +containers +distributive +indexed-traversable, + any.containers ==0.6.7, + any.contravariant ==1.5.5, + contravariant +semigroups +statevar +tagged, + any.data-fix ==0.3.3, + any.deepseq ==1.4.8.0, + any.directory ==1.3.7.1, + any.distributive ==0.6.2.1, + distributive +semigroups +tagged, + any.dlist ==1.0, + dlist -werror, + any.exceptions ==0.10.5, + any.fgl ==5.8.2.0, + fgl +containers042, + any.filepath ==1.4.2.2, + any.foldable1-classes-compat ==0.1, + foldable1-classes-compat +tagged, + any.generically ==0.1.1, + any.ghc-bignum ==1.3, + any.ghc-boot-th ==9.4.7, + any.ghc-prim ==0.9.1, + any.hashable ==1.4.6.0, + hashable -arch-native +integer-gmp -random-initial-seed, + any.indexed-traversable ==0.1.4, + any.indexed-traversable-instances ==0.1.2, + any.integer-conversion ==0.1.1, + any.integer-logarithms ==1.0.3.1, + integer-logarithms -check-bounds +integer-gmp, + any.mtl ==2.2.2, + any.network-uri ==2.6.4.2, + any.os-string ==2.0.3, + any.parsec ==3.1.16.1, + any.pretty ==1.1.3.6, + any.primitive ==0.9.0.0, + any.process ==1.6.17.0, + any.random ==1.2.1.2, + any.regex-base ==0.94.0.2, + any.regex-tdfa ==1.3.2.2, + regex-tdfa +doctest -force-o2, + any.rts ==1.0.2, + any.scientific ==0.3.8.0, + scientific -integer-simple, + any.semialign ==1.3.1, + semialign +semigroupoids, + any.semigroupoids ==6.0.1, + semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers, + any.splitmix ==0.1.0.5, + splitmix -optimised-mixer, + any.stm ==2.5.1.0, + any.strict ==0.5, + any.tagged ==0.8.8, + tagged +deepseq +transformers, + any.template-haskell ==2.19.0.0, + any.text ==2.0.2, + any.text-iso8601 ==0.1.1, + any.text-short ==0.1.6, + text-short -asserts, + any.th-abstraction ==0.7.0.0, + any.th-compat ==0.1.5, + any.these ==1.2.1, + any.time ==1.12.2, + any.time-compat ==1.9.7, + any.transformers ==0.5.6.2, + any.transformers-compat ==0.7.2, + transformers-compat -five +five-three -four +generic-deriving +mtl -three -two, + any.unix ==2.7.3, + any.unordered-containers ==0.2.20, + unordered-containers -debug, + any.uuid-types ==1.0.6, + any.vector ==0.13.1.0, + vector +boundschecks -internalchecks -unsafechecks -wall, + any.vector-stream ==0.1.0.1, + any.witherable ==0.5 +index-state: hackage.haskell.org 2024-06-17T00:48:51Z From 3946cbd4a0b477700172c04be7baa3f43777bc99 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Mon, 24 Jun 2024 05:12:21 +0000 Subject: [PATCH 22/74] Upgrade docker build images --- build/README.md | 4 + build/darwin.aarch64/build | 1 - build/darwin.x86_64/build | 1 - build/linux.aarch64/Dockerfile | 2 +- build/linux.aarch64/build | 1 - build/linux.armv6hf/Dockerfile | 64 ++++++---------- build/linux.armv6hf/build | 3 +- build/linux.armv6hf/cabal.project.freeze | 93 ++++++++++++++++++++++++ build/linux.armv6hf/scutil | 48 ++++++++++++ build/linux.riscv64/Dockerfile | 49 +++++-------- build/linux.riscv64/build | 4 +- build/windows.x86_64/build | 1 - 12 files changed, 194 insertions(+), 77 deletions(-) create mode 100644 build/linux.armv6hf/cabal.project.freeze create mode 100644 build/linux.armv6hf/scutil diff --git a/build/README.md b/build/README.md index eb745a0..31e8607 100644 --- a/build/README.md +++ b/build/README.md @@ -11,3 +11,7 @@ This makes it simple to build any release without exotic hardware or software. An image can be built and tagged using `build_builder`, and run on a source tarball using `run_builder`. + +Tip: Are you developing an image that relies on QEmu usermode emulation? + It's easy to accidentally depend on binfmt\_misc on the host OS. + Do a `echo 0 | sudo tee /proc/sys/fs/binfmt_misc/status` before testing. diff --git a/build/darwin.aarch64/build b/build/darwin.aarch64/build index 4235d3a..ff522ff 100755 --- a/build/darwin.aarch64/build +++ b/build/darwin.aarch64/build @@ -4,7 +4,6 @@ set -xe tar xzv --strip-components=1 chmod +x striptests && ./striptests mkdir "$TARGETNAME" - cabal update ( IFS=';'; cabal build $CABALOPTS ) find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; ls -l "$TARGETNAME" diff --git a/build/darwin.x86_64/build b/build/darwin.x86_64/build index 53857e8..058cece 100755 --- a/build/darwin.x86_64/build +++ b/build/darwin.x86_64/build @@ -4,7 +4,6 @@ set -xe tar xzv --strip-components=1 chmod +x striptests && ./striptests mkdir "$TARGETNAME" - cabal update ( IFS=';'; cabal build $CABALOPTS ) find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; ls -l "$TARGETNAME" diff --git a/build/linux.aarch64/Dockerfile b/build/linux.aarch64/Dockerfile index fadb6a4..1ffe1bd 100644 --- a/build/linux.aarch64/Dockerfile +++ b/build/linux.aarch64/Dockerfile @@ -28,7 +28,7 @@ RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-in # Due to an apparent cabal bug, we specify our options directly to cabal # It won't reuse caches if ghc-options are specified in ~/.cabal/config -ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg" +ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native" # Prebuild the dependencies RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck diff --git a/build/linux.aarch64/build b/build/linux.aarch64/build index f8001aa..3ce61ce 100755 --- a/build/linux.aarch64/build +++ b/build/linux.aarch64/build @@ -4,7 +4,6 @@ set -xe tar xzv --strip-components=1 chmod +x striptests && ./striptests mkdir "$TARGETNAME" - cabal update ( IFS=';'; cabal build $CABALOPTS --enable-executable-static ) find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; ls -l "$TARGETNAME" diff --git a/build/linux.armv6hf/Dockerfile b/build/linux.armv6hf/Dockerfile index f933dda..b4d4197 100644 --- a/build/linux.armv6hf/Dockerfile +++ b/build/linux.armv6hf/Dockerfile @@ -1,25 +1,7 @@ -# I've again spent days trying to get a working armv6hf compiler going. -# God only knows how many recompilations of GCC, GHC, libraries, and -# ShellCheck itself, has gone into it. -# -# I tried Debian's toolchain. I tried my custom one built according to -# RPi `gcc -v`. I tried GHC9, glibc, musl, registerised vs not, but -# nothing has yielded an armv6hf binary that does not immediately -# segfault on qemu-arm-static or the RPi itself. -# -# I then tried the same but with armv7hf. Same story. -# -# Emulating the entire userspace with balenalib again? Very strange build -# failures where programs would fail to execute with > ~100 arguments. -# -# Finally, creating our own appears to work when using a custom QEmu -# patched to follow execve calls. -# -# PS: $100 bounty for getting a RPi1 compatible static build going -# with cross-compilation, similar to what the aarch64 build does. -# +# This Docker file uses a custom QEmu fork with patches to follow execve +# to build all of ShellCheck emulated. -FROM ubuntu:20.04 +FROM ubuntu:24.04 ENV TARGETNAME linux.armv6hf @@ -27,34 +9,34 @@ ENV TARGETNAME linux.armv6hf USER root ENV DEBIAN_FRONTEND noninteractive RUN apt-get update -RUN apt-get install -y build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev -WORKDIR /build -RUN git clone --depth 1 https://github.com/koalaman/qemu -RUN cd qemu && ./configure --static && cd build && ninja qemu-arm -RUN cp qemu/build/qemu-arm /build/qemu-arm-static +RUN apt-get install -y --no-install-recommends build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev python3-setuptools ca-certificates debootstrap +WORKDIR /qemu +RUN git clone --depth 1 https://github.com/koalaman/qemu . +RUN ./configure --static --disable-werror && cd build && ninja qemu-arm ENV QEMU_EXECVE 1 +# Convenience utility +COPY scutil /bin/scutil +COPY scutil /chroot/bin/scutil +RUN chmod +x /bin/scutil /chroot/bin/scutil + # Set up an armv6 userspace WORKDIR / -RUN apt-get install -y debootstrap qemu-user-static -# We expect this to fail if the host doesn't have binfmt qemu support -RUN qemu-debootstrap --arch armhf bullseye pi http://mirrordirector.raspbian.org/raspbian || [ -e /pi/etc/issue ] -RUN cp /build/qemu-arm-static /pi/usr/bin/qemu-arm-static -RUN printf > /bin/pirun '%s\n' '#!/bin/sh' 'chroot /pi /usr/bin/qemu-arm-static /usr/bin/env "$@"' && chmod +x /bin/pirun -# If the debootstrap process didn't finish, continue it -RUN [ ! -e /pi/debootstrap ] || pirun '/debootstrap/debootstrap' --second-stage +RUN debootstrap --arch armhf --variant=minbase --foreign bookworm /chroot http://mirrordirector.raspbian.org/raspbian +RUN cp /qemu/build/qemu-arm /chroot/bin/qemu +RUN scutil emu /debootstrap/debootstrap --second-stage # Install deps in the chroot -RUN pirun apt-get update -RUN pirun apt-get install -y ghc cabal-install +RUN scutil emu apt-get update +RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install +RUN scutil emu cabal update # Finally we can build the current dependencies. This takes hours. ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections" -RUN pirun cabal update -RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck -RUN IFS=';' && pirun cabal install $CABALOPTS --lib fgl +# Generated with `cabal freeze --constraint 'hashable -arch-native'` +COPY cabal.project.freeze /chroot/etc +RUN IFS=";" && scutil install_from_freeze /chroot/etc/cabal.project.freeze emu cabal install $CABALOPTS # Copy the build script -WORKDIR /pi/scratch -COPY build /pi/usr/bin -ENTRYPOINT ["/bin/pirun", "/usr/bin/build"] +COPY build /chroot/bin +ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"] diff --git a/build/linux.armv6hf/build b/build/linux.armv6hf/build index daa94d9..1d496ae 100755 --- a/build/linux.armv6hf/build +++ b/build/linux.armv6hf/build @@ -1,8 +1,9 @@ #!/bin/sh set -xe -cd /scratch +mkdir /scratch && cd /scratch { tar xzv --strip-components=1 + cp /etc/cabal.project.freeze . chmod +x striptests && ./striptests mkdir "$TARGETNAME" # This script does not cabal update because compiling anything new is slow diff --git a/build/linux.armv6hf/cabal.project.freeze b/build/linux.armv6hf/cabal.project.freeze new file mode 100644 index 0000000..183bcc6 --- /dev/null +++ b/build/linux.armv6hf/cabal.project.freeze @@ -0,0 +1,93 @@ +active-repositories: hackage.haskell.org:merge +constraints: any.Diff ==0.5, + any.OneTuple ==0.4.2, + any.QuickCheck ==2.14.3, + QuickCheck -old-random +templatehaskell, + any.StateVar ==1.2.2, + any.aeson ==2.2.3.0, + aeson +ordered-keymap, + any.array ==0.5.4.0, + any.assoc ==1.1.1, + assoc -tagged, + any.base ==4.15.1.0, + any.base-orphans ==0.9.2, + any.bifunctors ==5.6.2, + bifunctors +tagged, + any.binary ==0.8.8.0, + any.bytestring ==0.10.12.1, + any.character-ps ==0.1, + any.comonad ==5.0.8, + comonad +containers +distributive +indexed-traversable, + any.containers ==0.6.4.1, + any.contravariant ==1.5.5, + contravariant +semigroups +statevar +tagged, + any.data-array-byte ==0.1.0.1, + any.data-fix ==0.3.3, + any.deepseq ==1.4.5.0, + any.directory ==1.3.6.2, + any.distributive ==0.6.2.1, + distributive +semigroups +tagged, + any.dlist ==1.0, + dlist -werror, + any.exceptions ==0.10.4, + any.fgl ==5.8.2.0, + fgl +containers042, + any.filepath ==1.4.2.1, + any.foldable1-classes-compat ==0.1, + foldable1-classes-compat +tagged, + any.generically ==0.1.1, + any.ghc-bignum ==1.1, + any.ghc-boot-th ==9.0.2, + any.ghc-prim ==0.7.0, + any.hashable ==1.4.6.0, + hashable -arch-native +integer-gmp -random-initial-seed, + any.indexed-traversable ==0.1.4, + any.indexed-traversable-instances ==0.1.2, + any.integer-conversion ==0.1.1, + any.integer-logarithms ==1.0.3.1, + integer-logarithms -check-bounds +integer-gmp, + any.mtl ==2.2.2, + any.network-uri ==2.6.4.2, + any.parsec ==3.1.14.0, + any.pretty ==1.1.3.6, + any.primitive ==0.9.0.0, + any.process ==1.6.13.2, + any.random ==1.2.1.2, + any.regex-base ==0.94.0.2, + any.regex-tdfa ==1.3.2.2, + regex-tdfa +doctest -force-o2, + any.rts ==1.0.2, + any.scientific ==0.3.8.0, + scientific -integer-simple, + any.semialign ==1.3.1, + semialign +semigroupoids, + any.semigroupoids ==6.0.1, + semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers, + any.splitmix ==0.1.0.5, + splitmix -optimised-mixer, + any.stm ==2.5.0.0, + any.strict ==0.5, + any.tagged ==0.8.8, + tagged +deepseq +transformers, + any.template-haskell ==2.17.0.0, + any.text ==1.2.5.0, + any.text-iso8601 ==0.1.1, + any.text-short ==0.1.6, + text-short -asserts, + any.th-abstraction ==0.7.0.0, + any.th-compat ==0.1.5, + any.these ==1.2.1, + any.time ==1.9.3, + any.time-compat ==1.9.7, + any.transformers ==0.5.6.2, + any.transformers-compat ==0.7.2, + transformers-compat -five +five-three -four +generic-deriving +mtl -three -two, + any.unix ==2.7.2.2, + any.unordered-containers ==0.2.20, + unordered-containers -debug, + any.uuid-types ==1.0.6, + any.vector ==0.13.1.0, + vector +boundschecks -internalchecks -unsafechecks -wall, + any.vector-stream ==0.1.0.1, + any.witherable ==0.5 +index-state: hackage.haskell.org 2024-06-18T02:21:19Z diff --git a/build/linux.armv6hf/scutil b/build/linux.armv6hf/scutil new file mode 100644 index 0000000..a85d810 --- /dev/null +++ b/build/linux.armv6hf/scutil @@ -0,0 +1,48 @@ +#!/bin/dash +# Various ShellCheck build utility functions + +# Generally set a ulimit to avoid QEmu using too much memory +ulimit -v "$((10*1024*1024))" +# If we happen to invoke or run under QEmu, make sure to follow execve. +# This requires a patched QEmu. +export QEMU_EXECVE=1 + +# Retry a command until it succeeds +# Usage: scutil retry 3 mycmd +retry() { + n="$1" + ret=1 + shift + while [ "$n" -gt 0 ] + do + "$@" + ret=$? + [ "$ret" = 0 ] && break + n=$((n-1)) + done + return "$ret" +} + +# Install all dependencies from a freeze file +# Usage: scutil install_from_freeze /path/cabal.project.freeze cabal install +install_from_freeze() { + linefeed=$(printf '\nx') + linefeed=${linefeed%x} + flags=$( + sed 's/constraints:/&\n /' "$1" | + grep -vw -e rts -e base | + sed -n -e 's/^ *\([^,]*\).*/\1/p' | + sed -e 's/any\.\([^ ]*\) ==\(.*\)/\1-\2/; te; s/.*/--constraint\n&/; :e') + shift + # shellcheck disable=SC2086 + ( IFS=$linefeed; set -x; "$@" $flags ) +} + +# Run a command under emulation. +# This assumes the correct emulator is named 'qemu' and the chroot is /chroot +# Usage: scutil emu echo "Hello World" +emu() { + chroot /chroot /bin/qemu /usr/bin/env "$@" +} + +"$@" diff --git a/build/linux.riscv64/Dockerfile b/build/linux.riscv64/Dockerfile index 0ac95e5..d138ff7 100644 --- a/build/linux.riscv64/Dockerfile +++ b/build/linux.riscv64/Dockerfile @@ -10,46 +10,37 @@ ENV DEBIAN_FRONTEND noninteractive RUN apt-get update -y # Install qemu -RUN apt-get install -y --no-install-recommends build-essential ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev curl ca-certificates python3-virtualenv git python3-setuptools +RUN apt-get install -y --no-install-recommends build-essential ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev curl ca-certificates python3-virtualenv git python3-setuptools debootstrap WORKDIR /qemu RUN git clone --depth 1 https://github.com/koalaman/qemu . -#RUN git clone https://github.com/balena-io/qemu . -# Release 7.0.0 -#RUN git checkout 639d1d8903f65d74eb04c49e0df7a4b2f014cd86 RUN ./configure --target-list=riscv64-linux-user --static --disable-system --disable-pie --disable-werror RUN cd build && ninja qemu-riscv64 ENV QEMU_EXECVE 1 +# Convenience utility +COPY scutil /bin/scutil +# We have to copy to /usr/bin because debootstrap will try to symlink /bin and fail if it exists +COPY scutil /chroot/usr/bin/scutil +RUN chmod +x /bin/scutil /chroot/usr/bin/scutil # Set up a riscv64 userspace -RUN apt-get install -y --no-install-recommends debootstrap -RUN debootstrap --arch=riscv64 --foreign noble /rvfs http://ports.ubuntu.com/ubuntu-ports -RUN cp /qemu/build/qemu-riscv64 /rvfs/usr/bin/qemu-riscv64-static - -# Command to run riscv binaries in the chroot. The Haskell runtime allocates 1TB -# vspace up front and QEmu has a RAM cost per vspace, so use ulimit to allocate -# less and reduce RAM usage. -RUN printf > /bin/rv '%s\n' '#!/bin/sh' 'ulimit -v $((10*1024*1024)); chroot /rvfs /usr/bin/qemu-riscv64-static /usr/bin/env "$@"' -RUN chmod +x /bin/rv -RUN [ ! -e /rvfs/debootstrap ] || rv '/debootstrap/debootstrap' --second-stage +WORKDIR / +RUN debootstrap --arch=riscv64 --variant=minbase --components=main,universe --foreign noble /chroot http://ports.ubuntu.com/ubuntu-ports +RUN cp /qemu/build/qemu-riscv64 /chroot/bin/qemu +RUN scutil emu /debootstrap/debootstrap --second-stage # Install deps in the chroot -RUN printf > /rvfs/etc/apt/sources.list '%s\n' 'deb http://ports.ubuntu.com/ubuntu-ports noble main universe' -RUN rv apt-get update -y -RUN rv apt-get install -y --no-install-recommends ghc cabal-install +RUN scutil emu apt-get update +RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install +RUN scutil emu cabal update -RUN rv cabal update # Generated with: cabal freeze -c 'hashable -arch-native'. We put it in /etc so cabal won't find it. -COPY cabal.project.freeze /rvfs/etc -# Awful hack to install everything from the freeze file -# This basically turns 'any.tagged ==0.8.8' into tagged-0.8.8 to install by version, -# and adds a -c before 'hashable -arch-native +integer-gmp' to make it a flag constraint. -RUN < /rvfs/etc/cabal.project.freeze sed 's/constraints:/&\n /' | grep -vw rts | sed -n -e 's/^ *\([^,]*\).*/\1/p' | sed -e 's/any\.\([^ ]*\) ==\(.*\)/\1-\2/; te; s/.*/-c\n&/; :e' > /tmp/preinstall-flags -# Finally we can build the current dependencies. This takes hours. -# There's apparently a random segfault during assembly, so retry a few times. -RUN set -x; IFS=${IFS# }; f() { rv cabal install --keep-going $(cat /tmp/preinstall-flags); }; for i in $(seq 5); do f; ret=$?; [ $ret = 0 ] && break; done; exit $ret +COPY cabal.project.freeze /chroot/etc + +# Build all dependencies from the freeze file. The emulator segfaults at random, +# so retry a few times. +RUN scutil install_from_freeze /chroot/etc/cabal.project.freeze retry 5 emu cabal install --keep-going # Copy the build script -WORKDIR /rvfs/scratch -COPY build /rvfs/usr/bin/build -ENTRYPOINT ["/bin/rv", "/usr/bin/build"] +COPY build /chroot/bin/build +ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"] diff --git a/build/linux.riscv64/build b/build/linux.riscv64/build index 63c487f..ed9dc27 100755 --- a/build/linux.riscv64/build +++ b/build/linux.riscv64/build @@ -2,6 +2,8 @@ set -xe IFS=';' { + mkdir -p /tmp/scratch + cd /tmp/scratch tar xzv --strip-components=1 chmod +x striptests && ./striptests # Use a freeze file to ensure we use the same dependencies we cached during @@ -9,7 +11,7 @@ IFS=';' cp /etc/cabal.project.freeze . mkdir "$TARGETNAME" # Retry in case of random segfault - cabal build --enable-executable-static || cabal build --enable-executable-static + scutil retry 3 cabal build --enable-executable-static find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \; ls -l "$TARGETNAME" "$TARGET-strip" -s "$TARGETNAME/shellcheck" diff --git a/build/windows.x86_64/build b/build/windows.x86_64/build index 7bf186e..22e5b42 100755 --- a/build/windows.x86_64/build +++ b/build/windows.x86_64/build @@ -8,7 +8,6 @@ set -xe tar xzv --strip-components=1 chmod +x striptests && ./striptests mkdir "$TARGETNAME" - cabal update ( IFS=';'; cabal build $CABALOPTS ) find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \; ls -l "$TARGETNAME" From b408f546209bdd344177d2d778b38d3a77f0db78 Mon Sep 17 00:00:00 2001 From: "Joseph C. Sible" Date: Sun, 7 Jul 2024 01:03:40 -0400 Subject: [PATCH 23/74] Simplify invokedNodes --- src/ShellCheck/CFGAnalysis.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ShellCheck/CFGAnalysis.hs b/src/ShellCheck/CFGAnalysis.hs index 0b99c9f..8534d6f 100644 --- a/src/ShellCheck/CFGAnalysis.hs +++ b/src/ShellCheck/CFGAnalysis.hs @@ -1350,7 +1350,7 @@ analyzeControlFlow params t = -- All nodes we've touched invocations <- readSTRef $ cInvocations ctx - let invokedNodes = M.fromDistinctAscList $ map (\c -> (c, ())) $ S.toList $ M.keysSet $ groupByNode $ M.map snd invocations + let invokedNodes = M.fromSet (const ()) $ S.unions $ map (M.keysSet . snd) $ M.elems invocations -- Invoke all functions that were declared but not invoked -- This is so that we still get warnings for dead code From 61b7e66f809d9c2d1be06e5787ae6a98039e798e Mon Sep 17 00:00:00 2001 From: "Joseph C. Sible" Date: Sun, 7 Jul 2024 01:07:39 -0400 Subject: [PATCH 24/74] Use sets instead of maps that never use their values --- src/ShellCheck/Analytics.hs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index a89f940..621f70a 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -491,14 +491,14 @@ checkWrongArithmeticAssignment params (T_SimpleCommand id [T_Assignment _ _ _ _ sequence_ $ do str <- getNormalString val var:op:_ <- matchRegex regex str - Map.lookup var references + guard $ S.member var references return . warn (getId val) 2100 $ "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))" where regex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)([+*-]).+$" - references = foldl (flip ($)) Map.empty (map insertRef $ variableFlow params) + references = foldl (flip ($)) S.empty (map insertRef $ variableFlow params) insertRef (Assignment (_, _, name, _)) = - Map.insert name () + S.insert name insertRef _ = Prelude.id getNormalString (T_NormalWord _ words) = do @@ -974,32 +974,32 @@ prop_checkArrayWithoutIndex9 = verifyTree checkArrayWithoutIndex "read -r -a arr prop_checkArrayWithoutIndex10 = verifyTree checkArrayWithoutIndex "read -ra arr <<< 'foo bar'; echo \"$arr\"" prop_checkArrayWithoutIndex11 = verifyNotTree checkArrayWithoutIndex "read -rpfoobar r; r=42" checkArrayWithoutIndex params _ = - doVariableFlowAnalysis readF writeF defaultMap (variableFlow params) + doVariableFlowAnalysis readF writeF defaultSet (variableFlow params) where - defaultMap = Map.fromList $ map (\x -> (x,())) arrayVariables + defaultSet = S.fromList arrayVariables readF _ (T_DollarBraced id _ token) _ = do - map <- get + s <- get return . maybeToList $ do name <- getLiteralString token - assigned <- Map.lookup name map + guard $ S.member name s return $ makeComment WarningC id 2128 "Expanding an array without an index only gives the first element." readF _ _ _ = return [] writeF _ (T_Assignment id mode name [] _) _ (DataString _) = do - isArray <- gets (Map.member name) + isArray <- gets (S.member name) return $ if not isArray then [] else case mode of Assign -> [makeComment WarningC id 2178 "Variable was used as an array but is now assigned a string."] Append -> [makeComment WarningC id 2179 "Use array+=(\"item\") to append items to an array."] writeF _ t name (DataArray _) = do - modify (Map.insert name ()) + modify (S.insert name) return [] writeF _ expr name _ = do if isIndexed expr - then modify (Map.insert name ()) - else modify (Map.delete name) + then modify (S.insert name) + else modify (S.delete name) return [] isIndexed expr = @@ -3968,12 +3968,12 @@ prop_checkTranslatedStringVariable4 = verifyNot checkTranslatedStringVariable "v prop_checkTranslatedStringVariable5 = verifyNot checkTranslatedStringVariable "foo=var; bar=val2; $\"foo bar\"" checkTranslatedStringVariable params (T_DollarDoubleQuoted id [T_Literal _ s]) | all isVariableChar s - && Map.member s assignments + && S.member s assignments = warnWithFix id 2256 "This translated string is the name of a variable. Flip leading $ and \" if this should be a quoted substitution." (fix id) where - assignments = foldl (flip ($)) Map.empty (map insertAssignment $ variableFlow params) - insertAssignment (Assignment (_, token, name, _)) | isVariableName name = - Map.insert name token + assignments = foldl (flip ($)) S.empty (map insertAssignment $ variableFlow params) + insertAssignment (Assignment (_, _, name, _)) | isVariableName name = + S.insert name insertAssignment _ = Prelude.id fix id = fixWith [replaceStart id params 2 "\"$"] checkTranslatedStringVariable _ _ = return () From 8746c6e7f20fdaa2463ae008dc527375ab76b04e Mon Sep 17 00:00:00 2001 From: "Joseph C. Sible" Date: Sun, 7 Jul 2024 01:07:53 -0400 Subject: [PATCH 25/74] Switch the order of the maps to avoid unnecessary unionWith instead of union --- src/ShellCheck/CFGAnalysis.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ShellCheck/CFGAnalysis.hs b/src/ShellCheck/CFGAnalysis.hs index 8534d6f..27098b1 100644 --- a/src/ShellCheck/CFGAnalysis.hs +++ b/src/ShellCheck/CFGAnalysis.hs @@ -133,7 +133,7 @@ internalToExternal s = literalValue = Nothing } } - flatVars = M.unionsWith (\_ last -> last) $ map mapStorage [sGlobalValues s, sLocalValues s, sPrefixValues s] + flatVars = M.unions $ map mapStorage [sPrefixValues s, sLocalValues s, sGlobalValues s] -- Conveniently get the state before a token id getIncomingState :: CFGAnalysis -> Id -> Maybe ProgramState @@ -672,7 +672,7 @@ vmPatch base diff = _ | vmIsQuickEqual base diff -> diff _ -> VersionedMap { mapVersion = -1, - mapStorage = M.unionWith (flip const) (mapStorage base) (mapStorage diff) + mapStorage = M.union (mapStorage diff) (mapStorage base) } -- Set a variable. This includes properties. Applies it to the appropriate scope. @@ -1373,7 +1373,7 @@ analyzeControlFlow params t = -- Fill in the map with unreachable states for anything we didn't get to let baseStates = M.fromDistinctAscList $ map (\c -> (c, (unreachableState, unreachableState))) $ uncurry enumFromTo $ nodeRange $ cfGraph cfg - let allStates = M.unionWith (flip const) baseStates invokedStates + let allStates = M.union invokedStates baseStates -- Convert to external states let nodeToData = M.map (\(a,b) -> (internalToExternal a, internalToExternal b)) allStates From e5fdec970ae3bc16369b3ec289a4c4fbb9254751 Mon Sep 17 00:00:00 2001 From: "Joseph C. Sible" Date: Sun, 7 Jul 2024 01:08:18 -0400 Subject: [PATCH 26/74] Swap the order of the tuple returned by orderEdge --- src/ShellCheck/CFG.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ShellCheck/CFG.hs b/src/ShellCheck/CFG.hs index e1d3259..a720911 100644 --- a/src/ShellCheck/CFG.hs +++ b/src/ShellCheck/CFG.hs @@ -300,13 +300,13 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) = edgesToCollapse = S.fromList $ filter filterEdges regularEdges remapping :: M.Map Node Node - remapping = foldl' (\m (new, old) -> M.insert old new m) M.empty $ map orderEdge $ S.toList edgesToCollapse + remapping = foldl' (\m (old, new) -> M.insert old new m) M.empty $ map orderEdge $ S.toList edgesToCollapse recursiveRemapping = M.fromList $ map (\c -> (c, recursiveLookup remapping c)) $ M.keys remapping filterEdges (a,b,_) = a `S.member` candidateNodes && b `S.member` candidateNodes - orderEdge (a,b,_) = if a < b then (a,b) else (b,a) + orderEdge (a,b,_) = if a < b then (b,a) else (a,b) counter = foldl' (\map key -> M.insertWith (+) key 1 map) M.empty isRegularEdge (_, _, CFEFlow) = True isRegularEdge _ = False From 95c0cc2e4bdbad193ba3616a5ecfd2741b1f3807 Mon Sep 17 00:00:00 2001 From: "Joseph C. Sible" Date: Sun, 7 Jul 2024 01:09:17 -0400 Subject: [PATCH 27/74] Simplify removeUnnecessaryStructuralNodes --- src/ShellCheck/CFG.hs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/ShellCheck/CFG.hs b/src/ShellCheck/CFG.hs index a720911..dc56b58 100644 --- a/src/ShellCheck/CFG.hs +++ b/src/ShellCheck/CFG.hs @@ -295,19 +295,19 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) = regularEdges = filter isRegularEdge edges inDegree = counter $ map (\(from,to,_) -> from) regularEdges outDegree = counter $ map (\(from,to,_) -> to) regularEdges - structuralNodes = S.fromList $ map fst $ filter isStructural nodes + structuralNodes = S.fromList [node | (node, CFStructuralNode) <- nodes] candidateNodes = S.filter isLinear structuralNodes edgesToCollapse = S.fromList $ filter filterEdges regularEdges remapping :: M.Map Node Node - remapping = foldl' (\m (old, new) -> M.insert old new m) M.empty $ map orderEdge $ S.toList edgesToCollapse - recursiveRemapping = M.fromList $ map (\c -> (c, recursiveLookup remapping c)) $ M.keys remapping + remapping = M.fromList $ map orderEdge $ S.toList edgesToCollapse + recursiveRemapping = M.mapWithKey (\c _ -> recursiveLookup remapping c) remapping filterEdges (a,b,_) = a `S.member` candidateNodes && b `S.member` candidateNodes orderEdge (a,b,_) = if a < b then (b,a) else (a,b) - counter = foldl' (\map key -> M.insertWith (+) key 1 map) M.empty + counter = M.fromListWith (+) . map (\key -> (key, 1)) isRegularEdge (_, _, CFEFlow) = True isRegularEdge _ = False @@ -317,11 +317,6 @@ removeUnnecessaryStructuralNodes (nodes, edges, mapping, association) = Nothing -> node Just x -> recursiveLookup map x - isStructural (node, label) = - case label of - CFStructuralNode -> True - _ -> False - isLinear node = M.findWithDefault 0 node inDegree == 1 && M.findWithDefault 0 node outDegree == 1 From 98b8dc0720148d69ff924f89966c50dc2dda2fe3 Mon Sep 17 00:00:00 2001 From: "Joseph C. Sible" Date: Sun, 7 Jul 2024 01:11:00 -0400 Subject: [PATCH 28/74] Use fromList instead of reimplementing it in terms of foldl --- src/ShellCheck/Analytics.hs | 20 ++++---------------- src/ShellCheck/CFGAnalysis.hs | 2 +- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 621f70a..211993c 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -496,10 +496,7 @@ checkWrongArithmeticAssignment params (T_SimpleCommand id [T_Assignment _ _ _ _ "Use $((..)) for arithmetics, e.g. i=$((i " ++ op ++ " 2))" where regex = mkRegex "^([_a-zA-Z][_a-zA-Z0-9]*)([+*-]).+$" - references = foldl (flip ($)) S.empty (map insertRef $ variableFlow params) - insertRef (Assignment (_, _, name, _)) = - S.insert name - insertRef _ = Prelude.id + references = S.fromList [name | Assignment (_, _, name, _) <- variableFlow params] getNormalString (T_NormalWord _ words) = do parts <- mapM getLiterals words @@ -2380,15 +2377,9 @@ prop_checkUnused51 = verifyTree checkUnusedAssignments "x[y[z=1]]=1; echo ${x[@] checkUnusedAssignments params t = execWriter (mapM_ warnFor unused) where flow = variableFlow params - references = foldl (flip ($)) defaultMap (map insertRef flow) - insertRef (Reference (base, token, name)) = - Map.insert (stripSuffix name) () - insertRef _ = id + references = Map.union (Map.fromList [(stripSuffix name, ()) | Reference (base, token, name) <- flow]) defaultMap - assignments = foldl (flip ($)) Map.empty (map insertAssignment flow) - insertAssignment (Assignment (_, token, name, _)) | isVariableName name = - Map.insert name token - insertAssignment _ = id + assignments = Map.fromList [(name, token) | Assignment (_, token, name, _) <- flow, isVariableName name] unused = Map.assocs $ Map.difference assignments references @@ -3971,10 +3962,7 @@ checkTranslatedStringVariable params (T_DollarDoubleQuoted id [T_Literal _ s]) && S.member s assignments = warnWithFix id 2256 "This translated string is the name of a variable. Flip leading $ and \" if this should be a quoted substitution." (fix id) where - assignments = foldl (flip ($)) S.empty (map insertAssignment $ variableFlow params) - insertAssignment (Assignment (_, _, name, _)) | isVariableName name = - S.insert name - insertAssignment _ = Prelude.id + assignments = S.fromList [name | Assignment (_, _, name, _) <- variableFlow params, isVariableName name] fix id = fixWith [replaceStart id params 2 "\"$"] checkTranslatedStringVariable _ _ = return () diff --git a/src/ShellCheck/CFGAnalysis.hs b/src/ShellCheck/CFGAnalysis.hs index 27098b1..cf982e0 100644 --- a/src/ShellCheck/CFGAnalysis.hs +++ b/src/ShellCheck/CFGAnalysis.hs @@ -1286,7 +1286,7 @@ dataflow ctx entry = do else do let (next, rest) = S.deleteFindMin ps nexts <- process states next - writeSTRef pending $ foldl (flip S.insert) rest nexts + writeSTRef pending $ S.union (S.fromList nexts) rest f (n-1) pending states process states node = do From 6593096ba06e6b54ec08d00a9c625930a357cdfe Mon Sep 17 00:00:00 2001 From: Sertonix Date: Fri, 28 Jun 2024 16:24:06 +0200 Subject: [PATCH 29/74] Allow SC3003 on busybox shell --- src/ShellCheck/Checks/ShellSupport.hs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs index cab0546..9192c0e 100644 --- a/src/ShellCheck/Checks/ShellSupport.hs +++ b/src/ShellCheck/Checks/ShellSupport.hs @@ -212,6 +212,8 @@ prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}" prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059 prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]" prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]" +prop_checkBashisms122 = verify checkBashisms "#!/bin/dash\n$'a'" +prop_checkBashisms123 = verifyNot checkBashisms "#!/bin/busybox sh\n$'a'" checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do params <- ask kludge params t @@ -229,7 +231,8 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is" bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is" - bashism (T_DollarSingleQuoted id _) = warnMsg id 3003 "$'..' is" + bashism (T_DollarSingleQuoted id _) = + unless isBusyboxSh $ warnMsg id 3003 "$'..' is" bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is" bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are" bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is" From 4c852749214e102f7a1daec08094ed91b08867f7 Mon Sep 17 00:00:00 2001 From: Sertonix Date: Tue, 2 Jul 2024 17:06:35 +0200 Subject: [PATCH 30/74] Fix SC3045 for busybox shell --- src/ShellCheck/Checks/ShellSupport.hs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs index 9192c0e..1207ad0 100644 --- a/src/ShellCheck/Checks/ShellSupport.hs +++ b/src/ShellCheck/Checks/ShellSupport.hs @@ -214,6 +214,9 @@ prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]" prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]" prop_checkBashisms122 = verify checkBashisms "#!/bin/dash\n$'a'" prop_checkBashisms123 = verifyNot checkBashisms "#!/bin/busybox sh\n$'a'" +prop_checkBashisms124 = verify checkBashisms "#!/bin/dash\ntype -p test" +prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test" +prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar" checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do params <- ask kludge params t @@ -446,10 +449,10 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do ("hash", Just $ if isDash then ["r", "v"] else ["r"]), ("jobs", Just ["l", "p"]), ("printf", Just []), - ("read", Just $ if isDash then ["r", "p"] else ["r"]), + ("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]), ("readonly", Just ["p"]), ("trap", Just []), - ("type", Just []), + ("type", Just $ if isBusyboxSh then ["p"] else []), ("ulimit", if isDash then Nothing else Just ["f"]), ("umask", Just ["S"]), ("unset", Just ["f", "v"]), From 6d2f3d8628235dd2146cd248d9828b004852c7ad Mon Sep 17 00:00:00 2001 From: Sertonix Date: Tue, 9 Jul 2024 15:12:16 +0200 Subject: [PATCH 31/74] Allow 'echo -e' in busybox shell --- src/ShellCheck/Checks/ShellSupport.hs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs index 1207ad0..2ec4cf7 100644 --- a/src/ShellCheck/Checks/ShellSupport.hs +++ b/src/ShellCheck/Checks/ShellSupport.hs @@ -217,6 +217,7 @@ prop_checkBashisms123 = verifyNot checkBashisms "#!/bin/busybox sh\n$'a'" prop_checkBashisms124 = verify checkBashisms "#!/bin/dash\ntype -p test" prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test" prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar" +prop_checkBashisms127 = verifyNot checkBashisms "#!/bin/busybox sh\necho -ne foo" checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do params <- ask kludge params t @@ -327,7 +328,11 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) | t `isCommand` "echo" && argString `matches` flagRegex = - if isDash + if isBusyboxSh + then + when (not (argString `matches` busyboxFlagRegex)) $ + warnMsg (getId arg) 3036 "echo flags besides -n and -e" + else if isDash then when (argString /= "-n") $ warnMsg (getId arg) 3036 "echo flags besides -n" @@ -336,6 +341,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do where argString = concat $ oversimplify arg flagRegex = mkRegex "^-[eEsn]+$" + busyboxFlagRegex = mkRegex "^-[en]+$" bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) | getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) = From d590a35ff8093a3a1edeac67cdb7fb9cffa593bf Mon Sep 17 00:00:00 2001 From: Hasit Mistry Date: Tue, 9 Jul 2024 14:22:19 -0700 Subject: [PATCH 32/74] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 366c427..03d4d4a 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ Services and platforms that have ShellCheck pre-installed and ready to use: * [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck) * [Github](https://github.com/features/actions) (only Linux) * [Trunk Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml) +* [CodeRabbit](https://coderabbit.ai/) Most other services, including [GitLab](https://about.gitlab.com/), let you install ShellCheck yourself, either through the system's package manager (see [Installing](#installing)), From 2696c6472dd55880f4dc1350262e1192953754ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Santoro?= Date: Wed, 31 Jul 2024 12:52:42 +0000 Subject: [PATCH 33/74] Whitelist oc to avoid SC2016 false positive Fixes #3033. --- src/ShellCheck/Analytics.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 211993c..08eed57 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -1097,6 +1097,7 @@ checkSingleQuotedVariables params t@(T_SingleQuoted id s) = ,"sudo" -- covering "sudo sh" and such ,"docker" -- like above ,"podman" + ,"oc" ,"dpkg-query" ,"jq" -- could also check that user provides --arg ,"rename" From 38c5ba7c79e35af29bb1496e774af1d5add0e73c Mon Sep 17 00:00:00 2001 From: Emil Berg Date: Sat, 3 Aug 2024 08:49:40 +0200 Subject: [PATCH 34/74] Fix typos and trailing whitespace --- .github/ISSUE_TEMPLATE.md | 4 ++-- README.md | 2 +- src/ShellCheck/AST.hs | 4 ++-- src/ShellCheck/Analytics.hs | 6 +++--- src/ShellCheck/CFG.hs | 2 +- src/ShellCheck/Checks/Custom.hs | 2 +- src/ShellCheck/Parser.hs | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 44d151e..493b465 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,6 +1,6 @@ #### For bugs -- Rule Id (if any, e.g. SC1000): -- My shellcheck version (`shellcheck --version` or "online"): +- Rule Id (if any, e.g. SC1000): +- My shellcheck version (`shellcheck --version` or "online"): - [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086) - [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit diff --git a/README.md b/README.md index 366c427..78802a0 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ Alternatively, you can download pre-compiled binaries for the latest release her * [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked) * [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked) * [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked) -* [macOS, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.aarch64.tar.xz) +* [macOS, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.aarch64.tar.xz) * [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz) * [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip) diff --git a/src/ShellCheck/AST.hs b/src/ShellCheck/AST.hs index ca05c98..979a9b0 100644 --- a/src/ShellCheck/AST.hs +++ b/src/ShellCheck/AST.hs @@ -206,7 +206,7 @@ pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t) pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c) pattern T_Array id t = OuterToken id (Inner_T_Array t) pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l) -pattern TA_Parentesis id t = OuterToken id (Inner_TA_Parenthesis t) +pattern TA_Parenthesis id t = OuterToken id (Inner_TA_Parenthesis t) pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value) pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3) pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1) @@ -259,7 +259,7 @@ pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l) pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l) pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l) -{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parentesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-} +{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parenthesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-} instance Eq Token where OuterToken _ a == OuterToken _ b = a == b diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 211993c..b3d673f 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -3294,7 +3294,7 @@ checkReturnAgainstZero params token = next@(TA_Unary _ "!" _):_ -> isOnlyTestInCommand next next@(TC_Group {}):_ -> isOnlyTestInCommand next next@(TA_Sequence _ [_]):_ -> isOnlyTestInCommand next - next@(TA_Parentesis _ _):_ -> isOnlyTestInCommand next + next@(TA_Parenthesis _ _):_ -> isOnlyTestInCommand next _ -> False -- TODO: Do better $? tracking and filter on whether @@ -4990,14 +4990,14 @@ checkUnnecessaryParens params t = T_ForArithmetic _ x y z _ -> mapM_ (checkLeading "for (((x); (y); (z))) is the same as for ((x; y; z))") [x,y,z] T_Assignment _ _ _ [t] _ -> checkLeading "a[(x)] is the same as a[x]" t T_Arithmetic _ t -> checkLeading "(( (x) )) is the same as (( x ))" t - TA_Parentesis _ (TA_Sequence _ [ TA_Parentesis id _ ]) -> + TA_Parenthesis _ (TA_Sequence _ [ TA_Parenthesis id _ ]) -> styleWithFix id 2322 "In arithmetic contexts, ((x)) is the same as (x). Prefer only one layer of parentheses." $ fix id _ -> return () where checkLeading str t = case t of - TA_Sequence _ [TA_Parentesis id _ ] -> styleWithFix id 2323 (str ++ ". Prefer not wrapping in additional parentheses.") $ fix id + TA_Sequence _ [TA_Parenthesis id _ ] -> styleWithFix id 2323 (str ++ ". Prefer not wrapping in additional parentheses.") $ fix id _ -> return () fix id = diff --git a/src/ShellCheck/CFG.hs b/src/ShellCheck/CFG.hs index dc56b58..81689a1 100644 --- a/src/ShellCheck/CFG.hs +++ b/src/ShellCheck/CFG.hs @@ -490,7 +490,7 @@ build t = do TA_Binary _ _ a b -> sequentially [a,b] TA_Expansion _ list -> sequentially list TA_Sequence _ list -> sequentially list - TA_Parentesis _ t -> build t + TA_Parenthesis _ t -> build t TA_Trinary _ cond a b -> do condition <- build cond diff --git a/src/ShellCheck/Checks/Custom.hs b/src/ShellCheck/Checks/Custom.hs index 76ac83c..17e9c9e 100644 --- a/src/ShellCheck/Checks/Custom.hs +++ b/src/ShellCheck/Checks/Custom.hs @@ -1,7 +1,7 @@ {- This empty file is provided for ease of patching in site specific checks. However, there are no guarantees regarding compatibility between versions. --} +-} {-# LANGUAGE TemplateHaskell #-} module ShellCheck.Checks.Custom (checker, ShellCheck.Checks.Custom.runTests) where diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs index 9cc5e02..dfe3131 100644 --- a/src/ShellCheck/Parser.hs +++ b/src/ShellCheck/Parser.hs @@ -827,7 +827,7 @@ readArithmeticContents = char ')' id <- endSpan start spacing - return $ TA_Parentesis id s + return $ TA_Parenthesis id s readArithTerm = readGroup <|> readVariable <|> readExpansion From c7611dfcc6ccb320b530a4e9179e6facee96a422 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Mon, 19 Aug 2024 18:37:29 -0700 Subject: [PATCH 35/74] Use dynamic artifact name to work around issue with v4 uploader --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8f9d7d0..3886655 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -65,7 +65,7 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: bin + name: ${{matrix.build}}.bin path: bin/ package_binary: From 68e6f02267defb1e2e6398b0f477b4b85e930c22 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sat, 31 Aug 2024 18:00:49 -0700 Subject: [PATCH 36/74] Expand list of recognized unicode spaces (and rewrite for performance) --- src/ShellCheck/Parser.hs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs index dfe3131..3de3537 100644 --- a/src/ShellCheck/Parser.hs +++ b/src/ShellCheck/Parser.hs @@ -141,15 +141,9 @@ carriageReturn = do parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ." return '\r' -almostSpace = - choice [ - check '\xA0' "unicode non-breaking space", - check '\x200B' "unicode zerowidth space" - ] - where - check c name = do - parseNote ErrorC 1018 $ "This is a " ++ name ++ ". Delete and retype it." - char c +almostSpace = do + parseNote ErrorC 1018 $ "This is a unicode space. Delete and retype it." + oneOf "\xA0\x2002\x2003\x2004\x2005\x2006\x2007\x2008\x2009\x200B\x202F" return ' ' --------- Message/position annotation on top of user state From 1487e57a46a48f926f0cd698756cfe41d9635c15 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sat, 31 Aug 2024 18:27:18 -0700 Subject: [PATCH 37/74] Suppress unused warnings about stderr and stderr_lines from bats tests, fixing tests. --- src/ShellCheck/Data.hs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ShellCheck/Data.hs b/src/ShellCheck/Data.hs index 917142e..3000a99 100644 --- a/src/ShellCheck/Data.hs +++ b/src/ShellCheck/Data.hs @@ -62,6 +62,9 @@ internalVariables = [ , "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP", "FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION", "flags_error", "flags_return" + + -- Bats + ,"stderr", "stderr_lines" ] specialIntegerVariables = [ From 88e441453ba3bbb2aa0449dc178b8f82aaee5b4c Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sat, 31 Aug 2024 18:31:47 -0700 Subject: [PATCH 38/74] Make SC2002 optional (useless-use-of-cat) --- src/ShellCheck/Analytics.hs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 869b683..ce11884 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -103,8 +103,7 @@ nodeChecksToTreeCheck checkList = nodeChecks :: [Parameters -> Token -> Writer [TokenComment] ()] nodeChecks = [ - checkUuoc - ,checkPipePitfalls + checkPipePitfalls ,checkForInQuoted ,checkForInLs ,checkShorthandIf @@ -273,6 +272,13 @@ optionalTreeChecks = [ cdPositive = "rm -r \"$(get_chroot_dir)/home\"", cdNegative = "set -e; dir=\"$(get_chroot_dir)\"; rm -r \"$dir/home\"" }, checkExtraMaskedReturns) + + ,(newCheckDescription { + cdName = "useless-use-of-cat", + cdDescription = "Check for Useless Use Of Cat (UUOC)", + cdPositive = "cat foo | grep bar", + cdNegative = "grep bar foo" + }, nodeChecksToTreeCheck [checkUuoc]) ] optionalCheckMap :: Map.Map String (Parameters -> Token -> [TokenComment]) From 8a1b24c7afcd0534e15eb7db8e386d0d550c6015 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 1 Sep 2024 13:21:44 -0700 Subject: [PATCH 39/74] Fix paths for CI binary packaging after upgrade --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3886655..b0d1085 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,7 @@ jobs: path: source/ build_source: - name: Build Source Code + name: Build needs: package_source strategy: matrix: @@ -80,13 +80,13 @@ jobs: uses: actions/download-artifact@v4 - name: Work around GitHub permissions bug - run: chmod +x bin/*/shellcheck* + run: chmod +x *.bin/*/shellcheck* - name: Package binaries run: | export TAGS="$(cat source/tags)" mkdir -p deploy - cp -r bin/* deploy + cp -r *.bin/* deploy cd deploy ../.prepare_deploy rm -rf */ README* LICENSE* From ca65071d778d140bc6bad64e699265dde25821e9 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 1 Sep 2024 14:06:26 -0700 Subject: [PATCH 40/74] Run unit tests in GitHub actions --- .github/workflows/build.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0d1085..83269c9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,6 +42,29 @@ jobs: name: source path: source/ + run_tests: + name: Run tests + needs: package_source + runs-on: ubuntu-latest + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + + - name: Install dependencies + run: | + sudo apt-get update && sudo apt-get install ghc cabal-install + cabal update + + - name: Unpack source + run: | + cd source + tar xvf source.tar.gz --strip-components=1 + + - name: Build and run tests + run: | + cd source + cabal test + build_source: name: Build needs: package_source From 79e43c4550aaf50ebcced1c2b2852f1bd2533f6c Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sat, 7 Sep 2024 17:14:52 -0700 Subject: [PATCH 41/74] Allow parsing arbitrary coproc names (fixes #3048) --- src/ShellCheck/AST.hs | 2 +- src/ShellCheck/AnalyzerLib.hs | 8 ++++++-- src/ShellCheck/CFG.hs | 14 +++++++++++--- src/ShellCheck/Parser.hs | 35 ++++++++++++++++++++++++++++------- 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/ShellCheck/AST.hs b/src/ShellCheck/AST.hs index 979a9b0..bafe035 100644 --- a/src/ShellCheck/AST.hs +++ b/src/ShellCheck/AST.hs @@ -138,7 +138,7 @@ data InnerToken t = | Inner_T_WhileExpression [t] [t] | Inner_T_Annotation [Annotation] t | Inner_T_Pipe String - | Inner_T_CoProc (Maybe String) t + | Inner_T_CoProc (Maybe Token) t | Inner_T_CoProcBody t | Inner_T_Include t | Inner_T_SourceCommand t t diff --git a/src/ShellCheck/AnalyzerLib.hs b/src/ShellCheck/AnalyzerLib.hs index 3b1faa9..531ce8b 100644 --- a/src/ShellCheck/AnalyzerLib.hs +++ b/src/ShellCheck/AnalyzerLib.hs @@ -559,8 +559,12 @@ getModifiedVariables t = T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo [(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op] - T_CoProc _ name _ -> - [(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)] + T_CoProc _ Nothing _ -> + [(t, t, "COPROC", DataArray SourceInteger)] + + T_CoProc _ (Just token) _ -> do + name <- maybeToList $ getLiteralString token + [(t, t, name, DataArray SourceInteger)] --Points to 'for' rather than variable T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)] diff --git a/src/ShellCheck/CFG.hs b/src/ShellCheck/CFG.hs index 81689a1..57aaf4b 100644 --- a/src/ShellCheck/CFG.hs +++ b/src/ShellCheck/CFG.hs @@ -668,10 +668,18 @@ build t = do status <- newNodeRange $ CFSetExitCode id linkRange cond status - T_CoProc id maybeName t -> do - let name = fromMaybe "COPROC" maybeName + T_CoProc id maybeNameToken t -> do + -- If unspecified, "COPROC". If not a constant string, Nothing. + let maybeName = case maybeNameToken of + Just x -> getLiteralString x + Nothing -> Just "COPROC" + + let parentNode = case maybeName of + Just str -> applySingle $ IdTagged id $ CFWriteVariable str CFValueArray + Nothing -> CFStructuralNode + start <- newStructuralNode - parent <- newNodeRange $ applySingle $ IdTagged id $ CFWriteVariable name CFValueArray + parent <- newNodeRange parentNode child <- subshell id "coproc" $ build t end <- newNodeRange $ CFSetExitCode id diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs index 3de3537..66d62ff 100644 --- a/src/ShellCheck/Parser.hs +++ b/src/ShellCheck/Parser.hs @@ -2795,17 +2795,29 @@ readFunctionDefinition = called "function" $ do prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }" prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }" prop_readCoProc3 = isOk readCoProc "coproc echo bar" +prop_readCoProc4 = isOk readCoProc "coproc a=b echo bar" +prop_readCoProc5 = isOk readCoProc "coproc 'foo' { echo bar; }" +prop_readCoProc6 = isOk readCoProc "coproc \"foo$$\" { echo bar; }" +prop_readCoProc7 = isOk readCoProc "coproc 'foo' ( echo bar )" +prop_readCoProc8 = isOk readCoProc "coproc \"foo$$\" while true; do true; done" readCoProc = called "coproc" $ do start <- startSpan try $ do string "coproc" - whitespace + spacing1 choice [ try $ readCompoundCoProc start, readSimpleCoProc start ] where readCompoundCoProc start = do - var <- optionMaybe $ - readVariableName `thenSkip` whitespace - body <- readBody readCompoundCommand + notFollowedBy2 readAssignmentWord + (var, body) <- choice [ + try $ do + body <- readBody readCompoundCommand + return (Nothing, body), + try $ do + var <- readNormalWord `thenSkip` spacing + body <- readBody readCompoundCommand + return (Just var, body) + ] id <- endSpan start return $ T_CoProc id var body readSimpleCoProc start = do @@ -3436,13 +3448,22 @@ isOk p s = parsesCleanly p s == Just True -- The string parses with no wa 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 +-- If the parser matches the string, return Right [ParseNotes+ParseProblems] +-- If it does not match the string, return Left [ParseProblems] +getParseOutput parser string = runIdentity $ do (res, sys) <- runParser testEnvironment (parser >> eof >> getState) "-" string case (res, sys) of (Right userState, systemState) -> - return $ Just . null $ parseNotes userState ++ parseProblems systemState - (Left _, _) -> return Nothing + return $ Right $ parseNotes userState ++ parseProblems systemState + (Left _, systemState) -> return $ Left $ parseProblems systemState + +-- If the parser matches the string, return Just whether it was clean (without emitting suggestions) +-- Otherwise, Nothing +parsesCleanly parser string = + case getParseOutput parser string of + Right list -> Just $ null list + Left _ -> Nothing parseWithNotes parser = do item <- parser From 5c2be767abff85fd10325a2d8a61a372567e63b8 Mon Sep 17 00:00:00 2001 From: Tony <3987237+random1223@users.noreply.github.com> Date: Mon, 9 Sep 2024 18:56:18 -0700 Subject: [PATCH 42/74] Update README.md Add Codety Scanner into the static analysis solution list. Here are the examples of the result: * Codety's pull request code review example: https://github.com/codetyio/codety-scanner/pull/66#issuecomment-2339438925 * Codety's GitHub code scan result example : https://github.com/codetyio/codety-scanner/runs/29907371258 Codety Scanner is open source: https://github.com/codetyio/codety-scanner --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c200a3e..9b776cf 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Services and platforms that have ShellCheck pre-installed and ready to use: * [Codacy](https://www.codacy.com/) * [Code Climate](https://codeclimate.com/) * [Code Factor](https://www.codefactor.io/) +* [Codety](https://www.codety.io/) via the [Codety Scanner](https://github.com/codetyio/codety-scanner) * [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck) * [Github](https://github.com/features/actions) (only Linux) * [Trunk Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml) From 5e3e98bcb0ca594185e9e675dac929aa053dd223 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 27 Oct 2024 15:43:30 -0700 Subject: [PATCH 43/74] Use CFG to determine use-before-define for SC2218 (fixes #3070) --- CHANGELOG.md | 1 + src/ShellCheck/Analytics.hs | 48 ++++++++++++++++++------------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43db00c..982036d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ ### Changed - SC2015 about `A && B || C` no longer triggers when B is a test command. ### Fixed +- SC2218 about function use-before-define is now more accurate. - SC2317 about unreachable commands is now less spammy for nested ones. - SC2292, optional suggestion for [[ ]], now triggers for Busybox. diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index ce11884..7f7a572 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -3765,32 +3765,32 @@ prop_checkUseBeforeDefinition1 = verifyTree checkUseBeforeDefinition "f; f() { t prop_checkUseBeforeDefinition2 = verifyNotTree checkUseBeforeDefinition "f() { true; }; f" prop_checkUseBeforeDefinition3 = verifyNotTree checkUseBeforeDefinition "if ! mycmd --version; then mycmd() { true; }; fi" prop_checkUseBeforeDefinition4 = verifyNotTree checkUseBeforeDefinition "mycmd || mycmd() { f; }" -checkUseBeforeDefinition _ t = - execWriter $ evalStateT (mapM_ examine $ revCommands) Map.empty +prop_checkUseBeforeDefinition5 = verifyTree checkUseBeforeDefinition "false || mycmd; mycmd() { f; }" +prop_checkUseBeforeDefinition6 = verifyNotTree checkUseBeforeDefinition "f() { one; }; f; f() { two; }; f" +checkUseBeforeDefinition :: Parameters -> Token -> [TokenComment] +checkUseBeforeDefinition params t = fromMaybe [] $ do + cfga <- cfgAnalysis params + let funcs = execState (doAnalysis findFunction t) Map.empty + -- Green cut: no point enumerating commands if there are no functions. + guard . not $ Map.null funcs + return $ execWriter $ doAnalysis (findInvocation cfga funcs) t where - examine t = case t of - T_Pipeline _ _ [T_Redirecting _ _ (T_Function _ _ _ name _)] -> - modify $ Map.insert name t - T_Annotation _ _ w -> examine w - T_Pipeline _ _ cmds -> do - m <- get - unless (Map.null m) $ - mapM_ (checkUsage m) $ concatMap recursiveSequences cmds - _ -> return () + findFunction t = + case t of + T_Function id _ _ name _ -> modify (Map.insertWith (++) name [id]) + _ -> return () - checkUsage map cmd = sequence_ $ do - name <- getCommandName cmd - def <- Map.lookup name map - return $ - err (getId cmd) 2218 - "This function is only defined later. Move the definition up." - - revCommands = reverse $ concat $ getCommandSequences t - recursiveSequences x = - let list = concat $ getCommandSequences x in - if null list - then [x] - else concatMap recursiveSequences list + findInvocation cfga funcs t = + case t of + T_SimpleCommand id _ (cmd:_) -> sequence_ $ do + name <- getLiteralString cmd + invocations <- Map.lookup name funcs + -- Is the function definitely being defined later? + guard $ any (\c -> CF.doesPostDominate cfga c id) invocations + -- Was one already defined, so it's actually a re-definition? + guard . not $ any (\c -> CF.doesPostDominate cfga id c) invocations + return $ err id 2218 "This function is only defined later. Move the definition up." + _ -> return () prop_checkForLoopGlobVariables1 = verify checkForLoopGlobVariables "for i in $var/*.txt; do true; done" prop_checkForLoopGlobVariables2 = verifyNot checkForLoopGlobVariables "for i in \"$var\"/*.txt; do true; done" From f2932ebcdc51527ca439737465e35b0b039a51b7 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 27 Oct 2024 16:02:56 -0700 Subject: [PATCH 44/74] Remember to add changelog to release messages (fixes #3051) --- test/check_release | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/check_release b/test/check_release index 665b265..f3ea9df 100755 --- a/test/check_release +++ b/test/check_release @@ -50,6 +50,11 @@ then fail "Expected git log message to be 'Stable version ...'" fi +if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]] +then + fail "Expected git log message to contain CHANGELOG" +fi + i=1 j=1 cat << EOF From 097018754b313a102834ec389079ee04673f68fa Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 27 Oct 2024 18:10:00 -0700 Subject: [PATCH 45/74] Mention that SC2002 (UUOC) is now no longer enabled by default. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 982036d..612068d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox. - Precompiled binaries for Linux riscv64 (linux.riscv64) ### Changed +- SC2002 about Useless Use Of Cat is now disabled by default. It can be + re-enabled with `--enable=useless-use-of-cat` or equivalent directive. - SC2015 about `A && B || C` no longer triggers when B is a test command. ### Fixed - SC2218 about function use-before-define is now more accurate. From 792466bc22ea9528313b5224621610551216e4a6 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 3 Nov 2024 13:56:51 -0800 Subject: [PATCH 46/74] Update Diff dependency (fixes #3075) --- ShellCheck.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShellCheck.cabal b/ShellCheck.cabal index fc52b12..0f604a9 100644 --- a/ShellCheck.cabal +++ b/ShellCheck.cabal @@ -53,7 +53,7 @@ library bytestring >= 0.10.6 && < 0.13, containers >= 0.5.6 && < 0.8, deepseq >= 1.4.1 && < 1.6, - Diff >= 0.4.0 && < 0.6, + Diff >= 0.4.0 && < 1.1, fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9), filepath >= 1.4.0 && < 1.5, mtl >= 2.2.2 && < 2.4, From 0ee46a0f33ebafde128e2c93dd45f2757de4d4ec Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 3 Nov 2024 14:19:08 -0800 Subject: [PATCH 47/74] Update filepath dependency --- ShellCheck.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShellCheck.cabal b/ShellCheck.cabal index 0f604a9..f31ead3 100644 --- a/ShellCheck.cabal +++ b/ShellCheck.cabal @@ -55,7 +55,7 @@ library deepseq >= 1.4.1 && < 1.6, Diff >= 0.4.0 && < 1.1, fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9), - filepath >= 1.4.0 && < 1.5, + filepath >= 1.4.0 && < 1.6, mtl >= 2.2.2 && < 2.4, parsec >= 3.1.14 && < 3.2, QuickCheck >= 2.14.2 && < 2.15, From 47bff1d5fdc478a3bfb32ffb532d33bab0e64b2c Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 3 Nov 2024 16:54:45 -0800 Subject: [PATCH 48/74] Add 24.04 to distrotest LTS --- test/distrotest | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/distrotest b/test/distrotest index ef467b8..c52a5c9 100755 --- a/test/distrotest +++ b/test/distrotest @@ -74,11 +74,12 @@ fedora:latest dnf install -y cabal-install ghc-template-haskell-devel fi archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel # Ubuntu LTS +ubuntu:24.04 apt-get update && apt-get install -y cabal-install ubuntu:22.04 apt-get update && apt-get install -y cabal-install ubuntu:20.04 apt-get update && apt-get install -y cabal-install # Stack on Ubuntu LTS -ubuntu:22.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest +ubuntu:24.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest EOF exit "$final" From 944d87915a191af442b3895ef5af89ffb65789d8 Mon Sep 17 00:00:00 2001 From: Evan Silberman Date: Mon, 11 Nov 2024 11:24:21 -0800 Subject: [PATCH 49/74] Recognize "oksh" executable name as ksh A portable version of OpenBSD's ksh is distributed with the executable name oksh [1]. It's a descendant of pdksh and can be shellchecked as ksh. [1]: https://github.com/ibara/oksh --- src/ShellCheck/Data.hs | 1 + src/ShellCheck/Parser.hs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ShellCheck/Data.hs b/src/ShellCheck/Data.hs index 3000a99..a145684 100644 --- a/src/ShellCheck/Data.hs +++ b/src/ShellCheck/Data.hs @@ -167,6 +167,7 @@ shellForExecutable name = "ksh" -> return Ksh "ksh88" -> return Ksh "ksh93" -> return Ksh + "oksh" -> return Ksh _ -> Nothing flagsForRead = "sreu:n:N:i:p:a:t:" diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs index 66d62ff..3986dab 100644 --- a/src/ShellCheck/Parser.hs +++ b/src/ShellCheck/Parser.hs @@ -3387,7 +3387,8 @@ readScriptFile sourced = do "busybox sh", "bash", "bats", - "ksh" + "ksh", + "oksh" ] badShells = [ "awk", From 7f3f014d49d4bc6b979ed5508b1c57874e795ee8 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Thu, 28 Nov 2024 11:51:22 -0800 Subject: [PATCH 50/74] Allow latest QuickCheck --- ShellCheck.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShellCheck.cabal b/ShellCheck.cabal index f31ead3..68c32d9 100644 --- a/ShellCheck.cabal +++ b/ShellCheck.cabal @@ -58,7 +58,7 @@ library filepath >= 1.4.0 && < 1.6, mtl >= 2.2.2 && < 2.4, parsec >= 3.1.14 && < 3.2, - QuickCheck >= 2.14.2 && < 2.15, + QuickCheck >= 2.14.2 && < 2.16, regex-tdfa >= 1.2.0 && < 1.4, transformers >= 0.4.2 && < 0.7, From 3c75d82db571aa3fd337e04077daa3c01e0c878e Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Fri, 29 Nov 2024 12:58:56 -0800 Subject: [PATCH 51/74] Fix stacktest complaining about permissions on /mnt --- test/distrotest | 6 +++--- test/stacktest | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/distrotest b/test/distrotest index c52a5c9..128ee44 100755 --- a/test/distrotest +++ b/test/distrotest @@ -17,13 +17,13 @@ and is still highly experimental. Make sure you're plugged in and have screen/tmux in place, then re-run with $0 --run to continue. -Also note that dist* will be deleted. +Also note that dist*/ and .stack-work/ will be deleted. EOF exit 0 } -echo "Deleting 'dist' and 'dist-newstyle'..." -rm -rf dist dist-newstyle +echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..." +rm -rf dist dist-newstyle .stack-work execs=$(find . -name shellcheck) diff --git a/test/stacktest b/test/stacktest index 9eb8d1e..b486c31 100755 --- a/test/stacktest +++ b/test/stacktest @@ -15,7 +15,7 @@ die() { echo "$*" >&2; exit 1; } command -v stack || die "stack is missing" -stack setup || die "Failed to setup with default resolver" +stack setup --allow-different-user || die "Failed to setup with default resolver" stack build --test || die "Failed to build/test with default resolver" # Nice to haves, but not necessary From 195b70db8c697e44b65beeb83abe4c283cd4cda4 Mon Sep 17 00:00:00 2001 From: "Joseph C. Sible" Date: Fri, 13 Dec 2024 23:06:49 -0500 Subject: [PATCH 52/74] Use unless instead of when and not --- src/ShellCheck/Checks/ShellSupport.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs index 2ec4cf7..f228832 100644 --- a/src/ShellCheck/Checks/ShellSupport.hs +++ b/src/ShellCheck/Checks/ShellSupport.hs @@ -330,7 +330,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do | t `isCommand` "echo" && argString `matches` flagRegex = if isBusyboxSh then - when (not (argString `matches` busyboxFlagRegex)) $ + unless (argString `matches` busyboxFlagRegex) $ warnMsg (getId arg) 3036 "echo flags besides -n and -e" else if isDash then From 0ecaf2b5f165497e461a47330ebbe11b05d8eb1a Mon Sep 17 00:00:00 2001 From: "Joseph C. Sible" Date: Fri, 13 Dec 2024 23:19:36 -0500 Subject: [PATCH 53/74] Use foldr instead of explicit recursion --- src/ShellCheck/Analytics.hs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 7f7a572..3a47ed9 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -5074,10 +5074,9 @@ checkExpansionWithRedirection params t = (T_Pipeline _ _ t@(_:_)) -> checkCmd id (last t) _ -> return () - checkCmd captureId (T_Redirecting _ redirs _) = walk captureId redirs + checkCmd captureId (T_Redirecting _ redirs _) = foldr (walk captureId) (return ()) redirs - walk captureId [] = return () - walk captureId (t:rest) = + walk captureId t acc = case t of T_FdRedirect _ _ (T_IoDuplicate _ _ "1") -> return () T_FdRedirect id "1" (T_IoDuplicate _ _ _) -> return () @@ -5086,7 +5085,7 @@ checkExpansionWithRedirection params t = if getLiteralString file == Just "/dev/null" then emit id captureId False else emit id captureId True - _ -> walk captureId rest + _ -> acc emit redirectId captureId suggestTee = do warn captureId 2327 "This command substitution will be empty because the command's output gets redirected away." From 5adfea21eec84a6bc8bbcaf4e22c02461dd850d9 Mon Sep 17 00:00:00 2001 From: "Joseph C. Sible" Date: Fri, 13 Dec 2024 23:20:48 -0500 Subject: [PATCH 54/74] Use the result of the comparison directly instead of an if/else --- src/ShellCheck/Analytics.hs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 3a47ed9..4757e57 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -5082,9 +5082,7 @@ checkExpansionWithRedirection params t = T_FdRedirect id "1" (T_IoDuplicate _ _ _) -> return () T_FdRedirect id "" (T_IoDuplicate _ op _) | op `elem` [T_GREATAND (Id 0), T_Greater (Id 0)] -> emit id captureId True T_FdRedirect id str (T_IoFile _ op file) | str `elem` ["", "1"] && op `elem` [ T_DGREAT (Id 0), T_Greater (Id 0) ] -> - if getLiteralString file == Just "/dev/null" - then emit id captureId False - else emit id captureId True + emit id captureId $ getLiteralString file /= Just "/dev/null" _ -> acc emit redirectId captureId suggestTee = do From 26b949b9b0b1552639fdf24411096cf00749be18 Mon Sep 17 00:00:00 2001 From: "Joseph C. Sible" Date: Fri, 13 Dec 2024 23:45:32 -0500 Subject: [PATCH 55/74] Use mapM_ instead of isJust and fromJust --- src/ShellCheck/Checks/Commands.hs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ShellCheck/Checks/Commands.hs b/src/ShellCheck/Checks/Commands.hs index c10016e..c37a67d 100644 --- a/src/ShellCheck/Checks/Commands.hs +++ b/src/ShellCheck/Checks/Commands.hs @@ -1431,9 +1431,8 @@ prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check where check t = do - cfga <- asks cfgAnalysis - when (isJust cfga) $ - foldM_ (perArg $ fromJust cfga) M.empty $ arguments t + maybeCfga <- asks cfgAnalysis + mapM_ (\cfga -> foldM_ (perArg cfga) M.empty $ arguments t) maybeCfga perArg cfga leftArgs t = case t of From 7deb7e853b4bc5d2ad90f021de180055e87611b5 Mon Sep 17 00:00:00 2001 From: "Joseph C. Sible" Date: Fri, 13 Dec 2024 23:47:55 -0500 Subject: [PATCH 56/74] Use mapM_ instead of sequence_ and <$> --- src/ShellCheck/Checks/ControlFlow.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ShellCheck/Checks/ControlFlow.hs b/src/ShellCheck/Checks/ControlFlow.hs index d23fa15..9f63141 100644 --- a/src/ShellCheck/Checks/ControlFlow.hs +++ b/src/ShellCheck/Checks/ControlFlow.hs @@ -78,7 +78,7 @@ controlFlowEffectChecks = [ runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck runNodeChecks perNode = do cfg <- asks cfgAnalysis - sequence_ $ runOnAll <$> cfg + mapM_ runOnAll cfg where getData datas n@(node, label) = do (pre, post) <- M.lookup node datas From d3001f337aa3f7653a621b302261f4eac01890d0 Mon Sep 17 00:00:00 2001 From: "Joseph C. Sible" Date: Fri, 13 Dec 2024 23:57:50 -0500 Subject: [PATCH 57/74] Simplify getParseOutput --- src/ShellCheck/Parser.hs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs index 66d62ff..9628b2e 100644 --- a/src/ShellCheck/Parser.hs +++ b/src/ShellCheck/Parser.hs @@ -3451,12 +3451,12 @@ isNotOk p s = parsesCleanly p s == Nothing -- The string does not parse -- If the parser matches the string, return Right [ParseNotes+ParseProblems] -- If it does not match the string, return Left [ParseProblems] getParseOutput parser string = runIdentity $ do - (res, sys) <- runParser testEnvironment - (parser >> eof >> getState) "-" string - case (res, sys) of - (Right userState, systemState) -> - return $ Right $ parseNotes userState ++ parseProblems systemState - (Left _, systemState) -> return $ Left $ parseProblems systemState + (res, systemState) <- runParser testEnvironment + (parser >> eof >> getState) "-" string + return $ case res of + Right userState -> + Right $ parseNotes userState ++ parseProblems systemState + Left _ -> Left $ parseProblems systemState -- If the parser matches the string, return Just whether it was clean (without emitting suggestions) -- Otherwise, Nothing From fe315a25c42219b8a7b0b16ffa69351421e2e997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lawrence=20Vel=C3=A1zquez?= Date: Sat, 28 Dec 2024 03:05:07 -0500 Subject: [PATCH 58/74] Recognize internal variables new in bash 5.3 From the bug-bash@gnu.org announcement "Bash-5.3-beta available": q. GLOBSORT: new variable to specify how to sort the results of pathname expansion (name, size, blocks, mtime, atime, ctime, none) in ascending or descending order. w. BASH_MONOSECONDS: new dynamic variable that returns the value of the system's monotonic clock, if one is available. x. BASH_TRAPSIG: new variable, set to the numeric signal number of the trap being executed while it's running. https://lists.gnu.org/archive/html/bug-bash/2024-12/msg00120.html --- src/ShellCheck/Data.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ShellCheck/Data.hs b/src/ShellCheck/Data.hs index 3000a99..3876507 100644 --- a/src/ShellCheck/Data.hs +++ b/src/ShellCheck/Data.hs @@ -49,6 +49,7 @@ internalVariables = [ "LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1", "PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR", + "BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT", "auto_resume", "histchars", -- Other @@ -78,7 +79,7 @@ variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [ "EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM", "READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE", - "HISTSIZE", "LINES" + "HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG" -- shflags , "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE" From cbf0b33463601fbd9827db31e08db424e3381074 Mon Sep 17 00:00:00 2001 From: Adrian Fluturel Date: Tue, 7 Jan 2025 03:24:29 +0100 Subject: [PATCH 59/74] Skip SC2015 when the last command is true --- src/ShellCheck/Analytics.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 4757e57..1329c86 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -881,13 +881,15 @@ prop_checkShorthandIf6 = verifyNot checkShorthandIf "if foo && bar || baz; then prop_checkShorthandIf7 = verifyNot checkShorthandIf "while foo && bar || baz; do true; done" prop_checkShorthandIf8 = verify checkShorthandIf "if true; then foo && bar || baz; fi" prop_checkShorthandIf9 = verifyNot checkShorthandIf "foo && [ -x /file ] || bar" +prop_checkShorthandIf10 = verifyNot checkShorthandIf "foo && bar || true" +prop_checkShorthandIf11 = verify checkShorthandIf "foo && bar || false" checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ b) (T_Pipeline _ _ t)) | not (isOk t || inCondition) && not (isTestCommand b) = info id 2015 "Note that A && B || C is not if-then-else. C may run when A is true." where isOk [t] = isAssignment t || fromMaybe False (do name <- getCommandBasename t - return $ name `elem` ["echo", "exit", "return", "printf"]) + return $ name `elem` ["echo", "exit", "return", "printf", "true"]) isOk _ = False inCondition = isCondition $ getPath (parentMap params) x checkShorthandIf _ _ = return () From 3a9ddae06b7e2293ebf61eaf8c71b01bbb769614 Mon Sep 17 00:00:00 2001 From: Eisuke Kawashima Date: Mon, 24 Mar 2025 05:49:06 +0900 Subject: [PATCH 60/74] fix(SC3013)!: remove SC3013 since the operators are specified by POSIX.1-2024 https://pubs.opengroup.org/onlinepubs/9799919799/utilities/test.html fix #3167 --- CHANGELOG.md | 2 ++ src/ShellCheck/Checks/ShellSupport.hs | 8 +------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 612068d..1a24b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ - SC2317 about unreachable commands is now less spammy for nested ones. - SC2292, optional suggestion for [[ ]], now triggers for Busybox. +### Removed +- SC3013: removed since the operators `-op/-nt/-ef` are specified in POSIX.1-2024 ## v0.10.0 - 2024-03-07 ### Added diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs index f228832..1789b6f 100644 --- a/src/ShellCheck/Checks/ShellSupport.hs +++ b/src/ShellCheck/Checks/ShellSupport.hs @@ -86,7 +86,7 @@ checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" -prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]" +prop_checkBashisms2 = verifyNot checkBashisms "[ foo -nt bar ]" prop_checkBashisms3 = verify checkBashisms "echo $((i++))" prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)" prop_checkBashisms5 = verify checkBashisms "source file" @@ -252,12 +252,6 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] = unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is" - bashism (TC_Binary id SingleBracket op _ _) - | op `elem` [ "-ot", "-nt", "-ef" ] = - unless isDash $ warnMsg id 3013 $ op ++ " is" - bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) - | op `elem` [ "-ot", "-nt", "-ef" ] = - unless isDash $ warnMsg id 3013 $ op ++ " is" bashism (TC_Binary id SingleBracket "==" _ _) = unless isBusyboxSh $ warnMsg id 3014 "== in place of = is" bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just "==", rhs]) = From bc60607f9e5be0ea7528589410845aef2d6f10a3 Mon Sep 17 00:00:00 2001 From: Eisuke Kawashima Date: Mon, 24 Mar 2025 06:04:19 +0900 Subject: [PATCH 61/74] fix(SC3012)!: do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024 https://pubs.opengroup.org/onlinepubs/9799919799/utilities/test.html fix #3168 --- CHANGELOG.md | 1 + src/ShellCheck/Checks/ShellSupport.hs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 612068d..82630bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - SC2002 about Useless Use Of Cat is now disabled by default. It can be re-enabled with `--enable=useless-use-of-cat` or equivalent directive. - SC2015 about `A && B || C` no longer triggers when B is a test command. +- SC3012: Do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024 ### Fixed - SC2218 about function use-before-define is now more accurate. - SC2317 about unreachable commands is now less spammy for nested ones. diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs index f228832..2a34931 100644 --- a/src/ShellCheck/Checks/ShellSupport.hs +++ b/src/ShellCheck/Checks/ShellSupport.hs @@ -247,10 +247,10 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is" bashism (T_HereString id _) = warnMsg id 3011 "here-strings are" bashism (TC_Binary id SingleBracket op _ _) - | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] = + | op `elem` [ "<", ">", "<=", ">=", "\\<=", "\\>="] = unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is" bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) - | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] = + | op `elem` [ "<", ">", "<=", ">=", "\\<=", "\\>="] = unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is" bashism (TC_Binary id SingleBracket op _ _) | op `elem` [ "-ot", "-nt", "-ef" ] = From 4f628cbe2a617e76fe3686a79c3d1eaf443acc08 Mon Sep 17 00:00:00 2001 From: Eisuke Kawashima Date: Fri, 4 Apr 2025 17:31:07 +0900 Subject: [PATCH 62/74] feat: check tautologically-false conditionals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix #3179 — negation of SC2055, `[ x = y -a x = z]` - fix #3181 — negation of SC2056, `(( x == y && x == z ))` - fix #3180 — negation of SC2252, `[ x = y ] && [ x = z ]` --- src/ShellCheck/Analytics.hs | 50 +++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 4757e57..431e56b 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -123,6 +123,7 @@ nodeChecks = [ ,checkCaseAgainstGlob ,checkCommarrays ,checkOrNeq + ,checkAndEq ,checkEchoWc ,checkConstantIfs ,checkPipedAssignment @@ -1631,6 +1632,55 @@ checkOrNeq _ (T_OrIf id lhs rhs) = sequence_ $ do checkOrNeq _ _ = return () +prop_checkAndEq1 = verify checkAndEq "if [[ $lol -eq cow && $lol -eq foo ]]; then echo foo; fi" +prop_checkAndEq2 = verify checkAndEq "(( a==lol && a==foo ))" +prop_checkAndEq3 = verify checkAndEq "[ \"$a\" = lol && \"$a\" = foo ]" +prop_checkAndEq4 = verifyNot checkAndEq "[ a = $cow && b = $foo ]" +prop_checkAndEq5 = verifyNot checkAndEq "[[ $a = /home && $a = */public_html/* ]]" +prop_checkAndEq6 = verify checkAndEq "[ $a = a ] && [ $a = b ]" +prop_checkAndEq7 = verify checkAndEq "[ $a = a ] && [ $a = b ] || true" +prop_checkAndEq8 = verifyNot checkAndEq "[[ $a == x && $a == x ]]" +prop_checkAndEq9 = verifyNot checkAndEq "[ 0 -eq $FOO ] && [ 0 -eq $BAR ]" + +-- For test-level "and": [ x = y -a x = z ] +checkAndEq _ (TC_And id typ op (TC_Binary _ _ op1 lhs1 rhs1 ) (TC_Binary _ _ op2 lhs2 rhs2)) + | (op1 == op2 && (op1 == "-eq" || op1 == "=" || op1 == "==")) && lhs1 == lhs2 && rhs1 /= rhs2 && not (any isGlob [rhs1,rhs2]) = + warn id 2055 $ "You probably wanted " ++ (if typ == SingleBracket then "-o" else "||") ++ " here, otherwise it's always false." + +-- For arithmetic context "and" +checkAndEq _ (TA_Binary id "&&" (TA_Binary _ "==" word1 _) (TA_Binary _ "==" word2 _)) + | word1 == word2 = + warn id 2056 "You probably wanted || here, otherwise it's always false." + +-- For command level "and": [ x = y ] && [ x = z ] +checkAndEq _ (T_AndIf id lhs rhs) = sequence_ $ do + (lhs1, op1, rhs1) <- getExpr lhs + (lhs2, op2, rhs2) <- getExpr rhs + guard $ op1 == op2 && op1 `elem` ["-eq", "=", "=="] + guard $ lhs1 == lhs2 && rhs1 /= rhs2 + guard . not $ any isGlob [rhs1, rhs2] + return $ warn id 2252 "You probably wanted || here, otherwise it's always false." + where + getExpr x = + case x of + T_AndIf _ lhs _ -> getExpr lhs -- Fetches x and y in `T_AndIf x (T_AndIf y z)` + T_Pipeline _ _ [x] -> getExpr x + T_Redirecting _ _ c -> getExpr c + T_Condition _ _ c -> getExpr c + TC_Binary _ _ op lhs rhs -> orient (lhs, op, rhs) + _ -> Nothing + + -- Swap items so that the constant side is rhs (or Nothing if both/neither is constant) + orient (lhs, op, rhs) = + case (isConstant lhs, isConstant rhs) of + (True, False) -> return (rhs, op, lhs) + (False, True) -> return (lhs, op, rhs) + _ -> Nothing + + +checkAndEq _ _ = return () + + prop_checkValidCondOps1 = verify checkValidCondOps "[[ a -xz b ]]" prop_checkValidCondOps2 = verify checkValidCondOps "[ -M a ]" prop_checkValidCondOps2a = verifyNot checkValidCondOps "[ 3 \\> 2 ]" From 8ff0c5be7a85561d23aea762f0495c144896aee3 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 6 Apr 2025 19:26:54 -0700 Subject: [PATCH 63/74] Suppress SC2216 when piping to cp/mv/rm -i (fixes #3141). --- src/ShellCheck/Analytics.hs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 4757e57..86a3292 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -3596,6 +3596,8 @@ prop_checkPipeToNowhere17 = verify checkPipeToNowhere "echo World | cat << 'EOF' prop_checkPipeToNowhere18 = verifyNot checkPipeToNowhere "ls 1>&3 3>&1 3>&- | wc -l" prop_checkPipeToNowhere19 = verifyNot checkPipeToNowhere "find . -print0 | du --files0-from=/dev/stdin" prop_checkPipeToNowhere20 = verifyNot checkPipeToNowhere "find . | du --exclude-from=/dev/fd/0" +prop_checkPipeToNowhere21 = verifyNot checkPipeToNowhere "yes | cp -ri foo/* bar" +prop_checkPipeToNowhere22 = verifyNot checkPipeToNowhere "yes | rm --interactive *" data PipeType = StdoutPipe | StdoutStderrPipe | NoPipe deriving (Eq) checkPipeToNowhere :: Parameters -> Token -> WriterT [TokenComment] Identity () @@ -3661,6 +3663,7 @@ checkPipeToNowhere params t = commandSpecificException name cmd = case name of "du" -> any ((`elem` ["exclude-from", "files0-from"]) . snd) $ getAllFlags cmd + _ | name `elem` interactiveFlagCmds -> hasInteractiveFlag cmd _ -> False warnAboutDupes (n, list@(_:_:_)) = @@ -3684,7 +3687,7 @@ checkPipeToNowhere params t = name <- getCommandBasename cmd guard $ name `elem` nonReadingCommands guard . not $ hasAdditionalConsumers cmd - guard . not $ name `elem` ["cp", "mv", "rm"] && cmd `hasFlag` "i" + guard . not $ name `elem` interactiveFlagCmds && hasInteractiveFlag cmd let suggestion = if name == "echo" then "Did you want 'cat' instead?" @@ -3699,6 +3702,9 @@ checkPipeToNowhere params t = treeContains pred t = isNothing $ doAnalysis (guard . not . pred) t + interactiveFlagCmds = [ "cp", "mv", "rm" ] + hasInteractiveFlag cmd = cmd `hasFlag` "i" || cmd `hasFlag` "interactive" + mayConsume t = case t of T_ProcSub _ "<" _ -> True From 72af76f443b81bbcd10889df8caf19be1671f7a2 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sun, 6 Apr 2025 19:58:13 -0700 Subject: [PATCH 64/74] Supress SC2093 when execfail is set (fixes #3178) --- src/ShellCheck/Analytics.hs | 4 +++- src/ShellCheck/AnalyzerLib.hs | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 86a3292..e6a1fd6 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -1896,7 +1896,9 @@ prop_checkSpuriousExec8 = verifyNot checkSpuriousExec "exec {origout}>&1- >tmp.l prop_checkSpuriousExec9 = verify checkSpuriousExec "for file in rc.d/*; do exec \"$file\"; done" prop_checkSpuriousExec10 = verifyNot checkSpuriousExec "exec file; r=$?; printf >&2 'failed\n'; return $r" prop_checkSpuriousExec11 = verifyNot checkSpuriousExec "exec file; :" -checkSpuriousExec _ = doLists +prop_checkSpuriousExec12 = verifyNot checkSpuriousExec "#!/bin/bash\nshopt -s execfail; exec foo; exec bar; echo 'Error'; exit 1;" +prop_checkSpuriousExec13 = verify checkSpuriousExec "#!/bin/dash\nshopt -s execfail; exec foo; exec bar; echo 'Error'; exit 1;" +checkSpuriousExec params t = when (not $ hasExecfail params) $ doLists t where doLists (T_Script _ _ cmds) = doList cmds False doLists (T_BraceGroup _ cmds) = doList cmds False diff --git a/src/ShellCheck/AnalyzerLib.hs b/src/ShellCheck/AnalyzerLib.hs index 531ce8b..da528a4 100644 --- a/src/ShellCheck/AnalyzerLib.hs +++ b/src/ShellCheck/AnalyzerLib.hs @@ -89,6 +89,8 @@ data Parameters = Parameters { hasSetE :: Bool, -- Whether this script has 'set -o pipefail' anywhere. hasPipefail :: Bool, + -- Whether this script has 'shopt -s execfail' anywhere. + hasExecfail :: Bool, -- A linear (bad) analysis of data flow variableFlow :: [StackData], -- A map from Id to Token @@ -226,6 +228,10 @@ makeParameters spec = params BusyboxSh -> isOptionSet "pipefail" root Sh -> True Ksh -> isOptionSet "pipefail" root, + hasExecfail = + case shellType params of + Bash -> isOptionSet "execfail" root + _ -> False, shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec), idMap = getTokenMap root, parentMap = getParentTree root, From e4853af5b0f541d8070d9c76adb59ccd9b1b44f0 Mon Sep 17 00:00:00 2001 From: Eisuke Kawashima Date: Fri, 21 Mar 2025 18:49:06 +0900 Subject: [PATCH 65/74] doc: update man --- shellcheck.1.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/shellcheck.1.md b/shellcheck.1.md index b873e45..c768bfe 100644 --- a/shellcheck.1.md +++ b/shellcheck.1.md @@ -78,7 +78,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts. : Don't try to look for .shellcheckrc configuration files. ---rcfile\ RCFILE +**--rcfile** *RCFILE* : Prefer the specified configuration file over searching for one in the default locations. @@ -317,7 +317,7 @@ Here is an example `.shellcheckrc`: disable=SC2236 If no `.shellcheckrc` is found in any of the parent directories, ShellCheck -will look in `~/.shellcheckrc` followed by the XDG config directory +will look in `~/.shellcheckrc` followed by the `$XDG_CONFIG_HOME` (usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on Windows. Only the first file found will be used. @@ -403,4 +403,4 @@ see https://gnu.org/licenses/gpl.html # SEE ALSO -sh(1) bash(1) +sh(1) bash(1) dash(1) ksh(1) From 574c6d18fbbae18b65b244ce2b37c3dced452a5a Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Tue, 8 Apr 2025 10:23:10 -0700 Subject: [PATCH 66/74] Suggest using test -e instead of -a (fixes #3174). --- CHANGELOG.md | 1 + src/ShellCheck/Analytics.hs | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 612068d..6309192 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - SC2327/SC2328: Warn about capturing the output of redirected commands. - SC2329: Warn when (non-escaping) functions are never invoked. - SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox. +- SC2331: Suggest using standard -e instead of unary -a in tests. - Precompiled binaries for Linux riscv64 (linux.riscv64) ### Changed - SC2002 about Useless Use Of Cat is now disabled by default. It can be diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index e6a1fd6..4984d44 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -204,6 +204,7 @@ nodeChecks = [ ,checkUnnecessaryParens ,checkPlusEqualsNumber ,checkExpansionWithRedirection + ,checkUnaryTestA ] optionalChecks = map fst optionalTreeChecks @@ -5098,6 +5099,15 @@ checkExpansionWithRedirection params t = err redirectId 2328 $ "This redirection takes output away from the command substitution" ++ if suggestTee then " (use tee to duplicate)." else "." +prop_checkUnaryTestA1 = verify checkUnaryTestA "[ -a foo ]" +prop_checkUnaryTestA2 = verify checkUnaryTestA "[ ! -a foo ]" +prop_checkUnaryTestA3 = verifyNot checkUnaryTestA "[ foo -a bar ]" +checkUnaryTestA params t = + case t of + TC_Unary id _ "-a" _ -> + styleWithFix id 2331 "For file existence, prefer standard -e over legacy -a." $ + fixWith [replaceStart id params 2 "-e"] + _ -> return () return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) From c41f3a4b8ac4eb7bcff230928a47f8f92f15f49d Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Tue, 8 Apr 2025 10:53:52 -0700 Subject: [PATCH 67/74] Warn about [ ! -o opt ] (and -a) being unconditionally true (fixes #3174) --- CHANGELOG.md | 2 ++ src/ShellCheck/Checks/ShellSupport.hs | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6309192..bc646d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - SC2329: Warn when (non-escaping) functions are never invoked. - SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox. - SC2331: Suggest using standard -e instead of unary -a in tests. +- SC2332: Warn about `[ ! -o opt ]` being unconditionally true in Bash. +- SC3062: Warn about bashism `[ -o opt ]`. - Precompiled binaries for Linux riscv64 (linux.riscv64) ### Changed - SC2002 about Useless Use Of Cat is now disabled by default. It can be diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs index f228832..624d474 100644 --- a/src/ShellCheck/Checks/ShellSupport.hs +++ b/src/ShellCheck/Checks/ShellSupport.hs @@ -63,6 +63,7 @@ checks = [ ,checkPS1Assignments ,checkMultipleBangs ,checkBangAfterPipe + ,checkNegatedUnaryOps ] testChecker (ForShell _ t) = @@ -218,6 +219,7 @@ prop_checkBashisms124 = verify checkBashisms "#!/bin/dash\ntype -p test" prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test" prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar" prop_checkBashisms127 = verifyNot checkBashisms "#!/bin/busybox sh\necho -ne foo" +prop_checkBashisms128 = verify checkBashisms "#!/bin/dash\ntype -p test" checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do params <- ask kludge params t @@ -272,6 +274,8 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is" bashism (TC_Unary id _ "-a" _) = warnMsg id 3017 "unary -a in place of -e is" + bashism (TC_Unary id _ "-o" _) = + warnMsg id 3062 "unary -o to check options is" bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just "-a", _]) = warnMsg id 3017 "unary -a in place of -e is" bashism (TA_Unary id op _) @@ -649,5 +653,22 @@ checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary." _ -> return () + +prop_checkNegatedUnaryOps1 = verify checkNegatedUnaryOps "[ ! -o braceexpand ]" +prop_checkNegatedUnaryOps2 = verifyNot checkNegatedUnaryOps "[ -o braceexpand ]" +prop_checkNegatedUnaryOps3 = verifyNot checkNegatedUnaryOps "[[ ! -o braceexpand ]]" +prop_checkNegatedUnaryOps4 = verifyNot checkNegatedUnaryOps "! [ -o braceexpand ]" +prop_checkNegatedUnaryOps5 = verify checkNegatedUnaryOps "[ ! -a file ]" +checkNegatedUnaryOps = ForShell [Bash] f + where + f token = case token of + TC_Unary id SingleBracket "!" (TC_Unary _ _ op _) | op `elem` ["-a", "-o"] -> + err id 2332 $ msg op + _ -> return () + + msg "-o" = "[ ! -o opt ] is always true because -o becomes logical OR. Use [[ ]] or ! [ -o opt ]." + msg "-a" = "[ ! -a file ] is always true because -a becomes logical AND. Use -e instead." + msg _ = pleaseReport "unhandled negated unary message" + return [] runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |]) From 7fc992d0dc590f32bd7265e719757102f5ad3b76 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Tue, 8 Apr 2025 20:52:52 -0700 Subject: [PATCH 68/74] Suppress SC2119/SC2120 for ${1:-default} (fixes #2023) --- src/ShellCheck/Analytics.hs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 4984d44..85104d8 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -2825,6 +2825,8 @@ prop_checkUnpassedInFunctions11 = verifyNotTree checkUnpassedInFunctions "foo() prop_checkUnpassedInFunctions12 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${!var*}; }; foo;" prop_checkUnpassedInFunctions13 = verifyNotTree checkUnpassedInFunctions "# shellcheck disable=SC2120\nfoo() { echo $1; }\nfoo\n" prop_checkUnpassedInFunctions14 = verifyTree checkUnpassedInFunctions "foo() { echo $#; }; foo" +prop_checkUnpassedInFunctions15 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${1-x}; }; foo" +prop_checkUnpassedInFunctions16 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${1:-x}; }; foo" checkUnpassedInFunctions params root = execWriter $ mapM_ warnForGroup referenceGroups where @@ -2841,9 +2843,10 @@ checkUnpassedInFunctions params root = case x of Assignment (_, _, str, _) -> isPositional str _ -> False + isPositionalReference function x = case x of - Reference (_, t, str) -> isPositional str && t `isDirectChildOf` function + Reference (_, t, str) -> isPositional str && t `isDirectChildOf` function && not (hasDefaultValue t) _ -> False isDirectChildOf child parent = fromMaybe False $ do @@ -2857,6 +2860,7 @@ checkUnpassedInFunctions params root = referenceList :: [(String, Bool, Token)] referenceList = execWriter $ doAnalysis (sequence_ . checkCommand) root + checkCommand :: Token -> Maybe (Writer [(String, Bool, Token)] ()) checkCommand t@(T_SimpleCommand _ _ (cmd:args)) = do str <- getLiteralString cmd @@ -2867,6 +2871,21 @@ checkUnpassedInFunctions params root = isPositional str = str == "*" || str == "@" || str == "#" || (all isDigit str && str /= "0" && str /= "") + -- True if t is a variable that specifies a default value, + -- such as ${1-x} or ${1:-x}. + hasDefaultValue t = + case t of + T_DollarBraced _ True l -> + let str = concat $ oversimplify l + in isDefaultValueModifier $ getBracedModifier str + _ -> False + + isDefaultValueModifier str = + case str of + '-':_ -> True + ':':'-':_ -> True + _ -> False + isArgumentless (_, b, _) = b referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList updateWith x@(name, _, _) = Map.insertWith (++) name [x] From 553a80f77ad6426107fe5fefcdc950e64c420b1d Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Tue, 8 Apr 2025 21:21:50 -0700 Subject: [PATCH 69/74] Also ignore SC2119 for :? and :+. --- src/ShellCheck/Analytics.hs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 85104d8..9eec8ed 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -2827,6 +2827,8 @@ prop_checkUnpassedInFunctions13 = verifyNotTree checkUnpassedInFunctions "# shel prop_checkUnpassedInFunctions14 = verifyTree checkUnpassedInFunctions "foo() { echo $#; }; foo" prop_checkUnpassedInFunctions15 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${1-x}; }; foo" prop_checkUnpassedInFunctions16 = verifyNotTree checkUnpassedInFunctions "foo() { echo ${1:-x}; }; foo" +prop_checkUnpassedInFunctions17 = verifyNotTree checkUnpassedInFunctions "foo() { mycommand ${1+--verbose}; }; foo" +prop_checkUnpassedInFunctions18 = verifyNotTree checkUnpassedInFunctions "foo() { if mycheck; then foo ${1?Missing}; fi; }; foo" checkUnpassedInFunctions params root = execWriter $ mapM_ warnForGroup referenceGroups where @@ -2882,9 +2884,10 @@ checkUnpassedInFunctions params root = isDefaultValueModifier str = case str of - '-':_ -> True - ':':'-':_ -> True + ':':c:_ -> c `elem` handlesDefault + c:_ -> c `elem` handlesDefault _ -> False + where handlesDefault = "-+?" isArgumentless (_, b, _) = b referenceGroups = Map.elems $ foldr updateWith Map.empty referenceList From efb5a5a2741e690d43d5539809326d5e249fd9f2 Mon Sep 17 00:00:00 2001 From: Eisuke Kawashima Date: Tue, 25 Mar 2025 01:31:06 +0900 Subject: [PATCH 70/74] fix(SC3013): check POSIX-compliant unary operators for test and [ fix #2125 --- src/ShellCheck/Checks/ShellSupport.hs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs index 624d474..2039483 100644 --- a/src/ShellCheck/Checks/ShellSupport.hs +++ b/src/ShellCheck/Checks/ShellSupport.hs @@ -220,6 +220,9 @@ prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar" prop_checkBashisms127 = verifyNot checkBashisms "#!/bin/busybox sh\necho -ne foo" prop_checkBashisms128 = verify checkBashisms "#!/bin/dash\ntype -p test" +prop_checkBashisms129 = verify checkBashisms "#!/bin/sh\n[ -k /tmp ]" +prop_checkBashisms130 = verifyNot checkBashisms "#!/bin/dash\ntest -k /tmp" +prop_checkBashisms131 = verify checkBashisms "#!/bin/sh\n[ -o errexit ]" checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do params <- ask kludge params t @@ -254,6 +257,18 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] = unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is" + bashism (TC_Unary id _ op _) + | op `elem` [ "-k", "-G", "-O" ] = + unless isDash $ warnMsg id 3013 $ op ++ " is" + bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) + | op `elem` [ "-k", "-G", "-O" ] = + unless isDash $ warnMsg id 3013 $ op ++ " is" + bashism (TC_Unary id _ op _) + | op `elem` [ "-N", "-o", "-R" ] = + warnMsg id 3013 $ op ++ " is" + bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) + | op `elem` [ "-N", "-o", "-R" ] = + warnMsg id 3013 $ op ++ " is" bashism (TC_Binary id SingleBracket op _ _) | op `elem` [ "-ot", "-nt", "-ef" ] = unless isDash $ warnMsg id 3013 $ op ++ " is" From dc41f0cc5bdcf1c814b184892b20ee0d2822e95a Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Fri, 11 Apr 2025 14:14:09 -0700 Subject: [PATCH 71/74] Refactor checks for POSIX test flags --- src/ShellCheck/Checks/ShellSupport.hs | 98 +++++++++++++++------------ 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/src/ShellCheck/Checks/ShellSupport.hs b/src/ShellCheck/Checks/ShellSupport.hs index 2039483..c828555 100644 --- a/src/ShellCheck/Checks/ShellSupport.hs +++ b/src/ShellCheck/Checks/ShellSupport.hs @@ -251,48 +251,16 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do bashism (T_Condition id DoubleBracket _) = unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is" bashism (T_HereString id _) = warnMsg id 3011 "here-strings are" - bashism (TC_Binary id SingleBracket op _ _) - | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] = - unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is" - bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) - | op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] = - unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is" - bashism (TC_Unary id _ op _) - | op `elem` [ "-k", "-G", "-O" ] = - unless isDash $ warnMsg id 3013 $ op ++ " is" - bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) - | op `elem` [ "-k", "-G", "-O" ] = - unless isDash $ warnMsg id 3013 $ op ++ " is" - bashism (TC_Unary id _ op _) - | op `elem` [ "-N", "-o", "-R" ] = - warnMsg id 3013 $ op ++ " is" - bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) - | op `elem` [ "-N", "-o", "-R" ] = - warnMsg id 3013 $ op ++ " is" - bashism (TC_Binary id SingleBracket op _ _) - | op `elem` [ "-ot", "-nt", "-ef" ] = - unless isDash $ warnMsg id 3013 $ op ++ " is" - bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) - | op `elem` [ "-ot", "-nt", "-ef" ] = - unless isDash $ warnMsg id 3013 $ op ++ " is" - bashism (TC_Binary id SingleBracket "==" _ _) = - unless isBusyboxSh $ warnMsg id 3014 "== in place of = is" - bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just "==", rhs]) = - unless isBusyboxSh $ warnMsg id 3014 "== in place of = is" - bashism (TC_Binary id SingleBracket "=~" _ _) = - warnMsg id 3015 "=~ regex matching is" - bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just "=~", rhs]) = - warnMsg id 3015 "=~ regex matching is" - bashism (TC_Unary id SingleBracket "-v" _) = - warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is" - bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just "-v", _]) = - warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is" - bashism (TC_Unary id _ "-a" _) = - warnMsg id 3017 "unary -a in place of -e is" - bashism (TC_Unary id _ "-o" _) = - warnMsg id 3062 "unary -o to check options is" - bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just "-a", _]) = - warnMsg id 3017 "unary -a in place of -e is" + + bashism (TC_Binary id _ op _ _) = + checkTestOp bashismBinaryTestFlags op id + bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) = + checkTestOp bashismBinaryTestFlags op id + bashism (TC_Unary id _ op _) = + checkTestOp bashismUnaryTestFlags op id + bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) = + checkTestOp bashismUnaryTestFlags op id + bashism (TA_Unary id op _) | op `elem` [ "|++", "|--", "++|", "--|"] = warnMsg id 3018 $ filter (/= '|') op ++ " is" @@ -529,6 +497,52 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do Assignment (_, _, name, _) -> name == var _ -> False + checkTestOp table op id = sequence_ $ do + (code, shells, msg) <- Map.lookup op table + guard . not $ shellType params `elem` shells + return $ warnMsg id code (msg op) + + +buildTestFlagMap list = Map.fromList $ concatMap (\(x,y) -> map (\c -> (c,y)) x) list +bashismBinaryTestFlags = buildTestFlagMap [ + -- ([list of applicable flags], + -- (error code, exempt shells, message builder :: String -> String)), + -- + -- Distinct error codes allow the wiki to give more helpful, targeted + -- information. + (["<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="], + (3012, [Dash, BusyboxSh], \op -> "lexicographical " ++ op ++ " is")), + (["-nt", "-ot", "-ef"], + (3013, [Dash, BusyboxSh], \op -> op ++ " is")), + (["=="], + (3014, [BusyboxSh], \op -> op ++ " in place of = is")), + (["=~"], + (3015, [], \op -> op ++ " regex matching is")), + + ([], (0,[],const "")) + ] +bashismUnaryTestFlags = buildTestFlagMap [ + (["-v"], + (3016, [], \op -> "test " ++ op ++ " (in place of [ -n \"${var+x}\" ]) is")), + (["-a"], + (3017, [], \op -> "unary " ++ op ++ " in place of -e is")), + (["-o"], + (3062, [], \op -> "test " ++ op ++ " to check options is")), + (["-R"], + (3063, [], \op -> "test " ++ op ++ " and namerefs in general are")), + (["-N"], + (3064, [], \op -> "test " ++ op ++ " is")), + (["-k"], + (3065, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")), + (["-G"], + (3066, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")), + (["-O"], + (3067, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")), + + ([], (0,[],const "")) + ] + + prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')" prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")" prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')" From f78714e0f6070bc4efa2f7c11c1ad632e8c250a2 Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Fri, 11 Apr 2025 19:14:53 -0700 Subject: [PATCH 72/74] Add ":" alongside "true" for SC2015 --- src/ShellCheck/Analytics.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 5f8c84c..ac686ff 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -890,7 +890,7 @@ checkShorthandIf params x@(T_OrIf _ (T_AndIf id _ b) (T_Pipeline _ _ t)) where isOk [t] = isAssignment t || fromMaybe False (do name <- getCommandBasename t - return $ name `elem` ["echo", "exit", "return", "printf", "true"]) + return $ name `elem` ["echo", "exit", "return", "printf", "true", ":"]) isOk _ = False inCondition = isCondition $ getPath (parentMap params) x checkShorthandIf _ _ = return () From b381658dbc74ebe7141717f7723be3a8b39121f9 Mon Sep 17 00:00:00 2001 From: Ian Ehrenwald Date: Fri, 25 Apr 2025 14:11:07 -0400 Subject: [PATCH 73/74] Add python3 to the list of badShells --- src/ShellCheck/Parser.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs index d019d89..84c3ce4 100644 --- a/src/ShellCheck/Parser.hs +++ b/src/ShellCheck/Parser.hs @@ -3397,6 +3397,7 @@ readScriptFile sourced = do "fish", "perl", "python", + "python3", "ruby", "tcsh", "zsh" From 47d358c1d44a84a1d54a4118fba4cf7668a9225d Mon Sep 17 00:00:00 2001 From: Vidar Holen Date: Sat, 17 May 2025 00:55:50 +0000 Subject: [PATCH 74/74] Tighten SC2333/SC2334 to only trigger against literals. --- src/ShellCheck/ASTLib.hs | 6 ++++++ src/ShellCheck/Analytics.hs | 29 +++++++++++++++++++---------- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/ShellCheck/ASTLib.hs b/src/ShellCheck/ASTLib.hs index 6b26b22..1e1b9cd 100644 --- a/src/ShellCheck/ASTLib.hs +++ b/src/ShellCheck/ASTLib.hs @@ -446,6 +446,12 @@ getLiteralStringExt more = g -- Is this token a string literal? isLiteral t = isJust $ getLiteralString t +-- Is this token a string literal number? +isLiteralNumber t = fromMaybe False $ do + s <- getLiteralString t + guard $ all isDigit s + return True + -- Escape user data for messages. -- Messages generally avoid repeating user data, but sometimes it's helpful. e4m = escapeForMessage diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index 073e911..2e9a3bd 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -1635,8 +1635,8 @@ checkOrNeq _ (T_OrIf id lhs rhs) = sequence_ $ do checkOrNeq _ _ = return () -prop_checkAndEq1 = verify checkAndEq "if [[ $lol -eq cow && $lol -eq foo ]]; then echo foo; fi" -prop_checkAndEq2 = verify checkAndEq "(( a==lol && a==foo ))" +prop_checkAndEq1 = verifyNot checkAndEq "cow=0; foo=0; if [[ $lol -eq cow && $lol -eq foo ]]; then echo foo; fi" +prop_checkAndEq2 = verifyNot checkAndEq "lol=0 foo=0; (( a==lol && a==foo ))" prop_checkAndEq3 = verify checkAndEq "[ \"$a\" = lol && \"$a\" = foo ]" prop_checkAndEq4 = verifyNot checkAndEq "[ a = $cow && b = $foo ]" prop_checkAndEq5 = verifyNot checkAndEq "[[ $a = /home && $a = */public_html/* ]]" @@ -1644,25 +1644,34 @@ prop_checkAndEq6 = verify checkAndEq "[ $a = a ] && [ $a = b ]" prop_checkAndEq7 = verify checkAndEq "[ $a = a ] && [ $a = b ] || true" prop_checkAndEq8 = verifyNot checkAndEq "[[ $a == x && $a == x ]]" prop_checkAndEq9 = verifyNot checkAndEq "[ 0 -eq $FOO ] && [ 0 -eq $BAR ]" +prop_checkAndEq10 = verify checkAndEq "(( a == 1 && a == 2 ))" +prop_checkAndEq11 = verify checkAndEq "[ $x -eq 1 ] && [ $x -eq 2 ]" +prop_checkAndEq12 = verify checkAndEq "[ 1 -eq $x ] && [ $x -eq 2 ]" +prop_checkAndEq13 = verifyNot checkAndEq "[ 1 -eq $x ] && [ $x -eq 1 ]" +prop_checkAndEq14 = verifyNot checkAndEq "[ $a = $b ] && [ $a = $c ]" + +checkAndEqOperands "-eq" rhs1 rhs2 = isLiteralNumber rhs1 && isLiteralNumber rhs2 +checkAndEqOperands op rhs1 rhs2 | op == "=" || op == "==" = isLiteral rhs1 && isLiteral rhs2 +checkAndEqOperands _ _ _ = False -- For test-level "and": [ x = y -a x = z ] checkAndEq _ (TC_And id typ op (TC_Binary _ _ op1 lhs1 rhs1 ) (TC_Binary _ _ op2 lhs2 rhs2)) - | (op1 == op2 && (op1 == "-eq" || op1 == "=" || op1 == "==")) && lhs1 == lhs2 && rhs1 /= rhs2 && not (any isGlob [rhs1,rhs2]) = - warn id 2055 $ "You probably wanted " ++ (if typ == SingleBracket then "-o" else "||") ++ " here, otherwise it's always false." + | op1 == op2 && lhs1 == lhs2 && rhs1 /= rhs2 && checkAndEqOperands op1 rhs1 rhs2 = + warn id 2333 $ "You probably wanted " ++ (if typ == SingleBracket then "-o" else "||") ++ " here, otherwise it's always false." -- For arithmetic context "and" -checkAndEq _ (TA_Binary id "&&" (TA_Binary _ "==" word1 _) (TA_Binary _ "==" word2 _)) - | word1 == word2 = - warn id 2056 "You probably wanted || here, otherwise it's always false." +checkAndEq _ (TA_Binary id "&&" (TA_Binary _ "==" lhs1 rhs1) (TA_Binary _ "==" lhs2 rhs2)) + | lhs1 == lhs2 && isLiteralNumber rhs1 && isLiteralNumber rhs2 = + warn id 2334 "You probably wanted || here, otherwise it's always false." -- For command level "and": [ x = y ] && [ x = z ] checkAndEq _ (T_AndIf id lhs rhs) = sequence_ $ do (lhs1, op1, rhs1) <- getExpr lhs (lhs2, op2, rhs2) <- getExpr rhs - guard $ op1 == op2 && op1 `elem` ["-eq", "=", "=="] + guard $ op1 == op2 guard $ lhs1 == lhs2 && rhs1 /= rhs2 - guard . not $ any isGlob [rhs1, rhs2] - return $ warn id 2252 "You probably wanted || here, otherwise it's always false." + guard $ checkAndEqOperands op1 rhs1 rhs2 + return $ warn id 2333 "You probably wanted || here, otherwise it's always false." where getExpr x = case x of