From 733f7c060c8bd08a79329e107a49978cd7b9d62b Mon Sep 17 00:00:00 2001 From: Eljees <3.14hell@gmail.com> Date: Sun, 9 Aug 2026 12:29:38 +0000 Subject: [PATCH] Accept an escaped ! as negation in [ ] test expressions readCondGroup already reads its parentheses with `readRegularOrEscaped (string "(")`, so `[ \( x \) ]` parses, and SC1028 tells users to write them that way. readCondNot, however, matched a bare `char '!'`, so the equally valid `[ \! x ]` was not recognised as a negation: on its own it produced a bogus SC2057 "Unknown binary operator", and combined with escaped parentheses it failed to parse at all (SC1072/SC1073). Both bash and dash treat `!`, `\!` and `"!"` alike in `[ ]`, because test simply receives the argument `!`. Inside `[[ ]]` they do not: bash rejects `[[ \! -e foo ]]` with "conditional binary operator expected". The escaped form is therefore accepted only when parsing `[ ]`, leaving `[[ ]]` as it was. Fixes #3475 --- src/ShellCheck/Analytics.hs | 1 + src/ShellCheck/Parser.hs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ShellCheck/Analytics.hs b/src/ShellCheck/Analytics.hs index f6208e72b..2ae3a12de 100644 --- a/src/ShellCheck/Analytics.hs +++ b/src/ShellCheck/Analytics.hs @@ -1710,6 +1710,7 @@ prop_checkValidCondOps2 = verify checkValidCondOps "[ -M a ]" prop_checkValidCondOps2a = verifyNot checkValidCondOps "[ 3 \\> 2 ]" prop_checkValidCondOps3 = verifyNot checkValidCondOps "[ 1 = 2 -a 3 -ge 4 ]" prop_checkValidCondOps4 = verifyNot checkValidCondOps "[[ ! -v foo ]]" +prop_checkValidCondOps5 = verifyNot checkValidCondOps "[ \\! -e foo ]" checkValidCondOps _ (TC_Binary id _ s _ _) | s `notElem` binaryTestOps = warn id 2057 "Unknown binary operator." diff --git a/src/ShellCheck/Parser.hs b/src/ShellCheck/Parser.hs index 2902f9b99..b8a64608b 100644 --- a/src/ShellCheck/Parser.hs +++ b/src/ShellCheck/Parser.hs @@ -696,7 +696,7 @@ readConditionContents single = readCondNot = do start <- startSpan - char '!' + if single then void (try (readRegularOrEscaped (string "!"))) else void (char '!') id <- endSpan start spacingOrLf expr <- readCondExpr @@ -947,6 +947,7 @@ prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar" prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo" prop_readCondition28 = isOk readCondition "[[ x = [\"$1\"] ]]" prop_readCondition29 = isOk readCondition "[[ x = [*] ]]" +prop_readCondition30 = isOk readCondition "[ \\! \\( -e foo -a -e bar \\) ]" readCondition = called "test expression" $ do opos <- getPosition