From 6076f0b1daabc55915ffe2de888c4604ebdab815 Mon Sep 17 00:00:00 2001
From: Vidar Holen <spam@vidarholen.net>
Date: Sun, 10 May 2015 12:49:50 -0700
Subject: [PATCH] Parse variables and subexpressions in brace expansions

---
 ShellCheck/AST.hs       |  3 ++-
 ShellCheck/Analytics.hs | 15 +++++++++++++--
 ShellCheck/Parser.hs    | 40 +++++++++++++++++++++++++++++-----------
 3 files changed, 44 insertions(+), 14 deletions(-)

diff --git a/ShellCheck/AST.hs b/ShellCheck/AST.hs
index 89b7f28..cca4a05 100644
--- a/ShellCheck/AST.hs
+++ b/ShellCheck/AST.hs
@@ -53,7 +53,7 @@ data Token =
     | T_Backticked Id [Token]
     | T_Bang Id
     | T_Banged Id Token
-    | T_BraceExpansion Id String
+    | T_BraceExpansion Id [Token]
     | T_BraceGroup Id [Token]
     | T_CLOBBER Id
     | T_Case Id
@@ -171,6 +171,7 @@ analyze f g i =
     delve (T_DoubleQuoted id list) = dl list $ T_DoubleQuoted id
     delve (T_DollarDoubleQuoted id list) = dl list $ T_DollarDoubleQuoted id
     delve (T_DollarExpansion id list) = dl list $ T_DollarExpansion id
+    delve (T_BraceExpansion id list) = dl list $ T_BraceExpansion id
     delve (T_Backticked id list) = dl list $ T_Backticked id
     delve (T_DollarArithmetic id c) = d1 c $ T_DollarArithmetic id
     delve (T_DollarBracket id c) = d1 c $ T_DollarBracket id
diff --git a/ShellCheck/Analytics.hs b/ShellCheck/Analytics.hs
index f664fcc..c505f60 100644
--- a/ShellCheck/Analytics.hs
+++ b/ShellCheck/Analytics.hs
@@ -1280,8 +1280,18 @@ checkConstantNoary _ _ = return ()
 
 prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
 prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
-checkBraceExpansionVars _ (T_BraceExpansion id s) | "..$" `isInfixOf` s =
-    warn id 2051 "Bash doesn't support variables in brace range expansions."
+checkBraceExpansionVars _ (T_BraceExpansion id list) = mapM_ check list
+  where
+    check element =
+        when ("..$" `isInfixOf` toString element) $
+            warn id 2051 "Bash doesn't support variables in brace range expansions."
+    literalExt t =
+        case t of
+            T_DollarBraced {} -> return "$"
+            T_DollarExpansion {} -> return "$"
+            T_DollarArithmetic {} -> return "$"
+            otherwise -> return "-"
+    toString t = fromJust $ getLiteralStringExt literalExt t
 checkBraceExpansionVars _ _ = return ()
 
 prop_checkForDecimals = verify checkForDecimals "((3.14*c))"
@@ -2413,6 +2423,7 @@ prop_checkSpacefulness22= verifyNotTree checkSpacefulness "echo $\"$1\""
 prop_checkSpacefulness23= verifyNotTree checkSpacefulness "a=(1); echo ${a[@]}"
 prop_checkSpacefulness24= verifyTree checkSpacefulness "a='a    b'; cat <<< $a"
 prop_checkSpacefulness25= verifyTree checkSpacefulness "a='s/[0-9]//g'; sed $a"
+prop_checkSpacefulness26= verifyTree checkSpacefulness "a='foo bar'; echo {1,2,$a}"
 
 checkSpacefulness params t =
     doVariableFlowAnalysis readF writeF (Map.fromList defaults) (variableFlow params)
diff --git a/ShellCheck/Parser.hs b/ShellCheck/Parser.hs
index fd2cbea..f8522b4 100644
--- a/ShellCheck/Parser.hs
+++ b/ShellCheck/Parser.hs
@@ -237,6 +237,12 @@ attempting rest branch =
 orFail parser errorAction =
     try parser <|> (errorAction >>= fail)
 
+-- Construct a node with a parser, e.g. T_Literal `withParser` (readGenericLiteral ",")
+withParser node parser = do
+    id <- getNextId
+    contents <- parser
+    return $ node id contents
+
 wasIncluded p = option False (p >> return True)
 
 acceptButWarn parser level code note =
@@ -1045,16 +1051,29 @@ readGenericEscaped = do
 
 prop_readBraced = isOk readBraced "{1..4}"
 prop_readBraced2 = isOk readBraced "{foo,bar,\"baz lol\"}"
-readBraced = try $ do
-    let strip (T_Literal _ s) = return ("\"" ++ s ++ "\"")
-    id <- getNextId
-    char '{'
-    str <- many1 ((readDoubleQuotedLiteral >>= strip) <|> readGenericLiteral1 (oneOf "}\"" <|> whitespace))
-    char '}'
-    let result = concat str
-    unless (',' `elem` result || ".." `isInfixOf` result) $
-        fail "Not a brace expression"
-    return $ T_BraceExpansion id result
+prop_readBraced3 = isOk readBraced "{1,\\},2}"
+prop_readBraced4 = isOk readBraced "{1,{2,3}}"
+prop_readBraced5 = isOk readBraced "{JP{,E}G,jp{,e}g}"
+prop_readBraced6 = isOk readBraced "{foo,bar,$((${var}))}"
+readBraced = try braceExpansion
+  where
+    braceExpansion =
+        T_BraceExpansion `withParser` do
+            char '{'
+            elements <- bracedElement `sepBy1` char ','
+            char '}'
+            return elements
+    bracedElement =
+        T_NormalWord `withParser` do
+            many $ choice [
+                braceExpansion,
+                readDollarExpression,
+                readSingleQuoted,
+                readDoubleQuoted,
+                braceLiteral
+                ]
+    braceLiteral =
+        T_Literal `withParser` readGenericLiteral1 (oneOf "{}\"$'," <|> whitespace)
 
 readNormalDollar = readDollarExpression <|> readDollarDoubleQuote <|> readDollarSingleQuote <|> readDollarLonely
 readDoubleQuotedDollar = readDollarExpression <|> readDollarLonely
@@ -1078,7 +1097,6 @@ readDollarDoubleQuote = do
     doubleQuote <?> "end of translated double quoted string"
     return $ T_DollarDoubleQuoted id x
 
-
 prop_readDollarArithmetic = isOk readDollarArithmetic "$(( 3 * 4 +5))"
 prop_readDollarArithmetic2 = isOk readDollarArithmetic "$(((3*4)+(1*2+(3-1))))"
 readDollarArithmetic = called "$((..)) expression" $ do