Repository: aws-cloudformation/cfn-lint

Issue:
Leading Whitespace in GetAtt syntax of !Sub not ignored
*cfn-lint version: cfn-lint 0.4.1*

*Description of issue.*

When using the `${}` syntax in a sub it doesn't ignore whitespace and throws an error, e.g.

```
"Resource": {
    "Fn::Sub": "${ AppendNotificationWatchLambdaLogGroup.Arn}*"
}
```

Will throw the following error:

```
E1019 String parameter  AppendNotificationWatchLambdaLogGroup.Arn not found in string for Resources/AppendNotificationWatchLambdaRolePolicy/Properties/PolicyDocument/Statement/0/Resource/Fn::Sub
```

When the whitespace is removed this works perfectly fine in cfn-lint (and always works in CloudFormation).


Discussion:
Good point.  I will work on a fix for this.  Thank you for the issue should have a fix shortly.
Great! Let me know if there is anything I can help with. At a quick glance I wasn't sure where to implement this in the code, so didn't send a PR. If you let me know which part this is implemented in I'm happy to send a PR

Resolving patch:
diff --git a/src/cfnlint/__init__.py b/src/cfnlint/__init__.py
--- a/src/cfnlint/__init__.py
+++ b/src/cfnlint/__init__.py
@@ -690,6 +690,17 @@ def _loc(self, obj):
         LOGGER.debug('Get location of object...')
         return (obj.start_mark.line, obj.start_mark.column, obj.end_mark.line, obj.end_mark.column)
 
+    def get_sub_parameters(self, sub_string):
+        """ Gets the parameters out of a Sub String"""
+        regex = re.compile(r'\${[^!].*?}')
+        string_params = regex.findall(sub_string)
+
+        results = []
+        for string_param in string_params:
+            results.append(string_param[2:-1].strip())
+
+        return results
+
     def get_location_yaml(self, text, path):
         """
         Get the location information
diff --git a/src/cfnlint/rules/functions/Sub.py b/src/cfnlint/rules/functions/Sub.py
--- a/src/cfnlint/rules/functions/Sub.py
+++ b/src/cfnlint/rules/functions/Sub.py
@@ -14,7 +14,6 @@
   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 """
-import re
 import six
 from cfnlint import CloudFormationLintRule
 from cfnlint import RuleMatch
@@ -32,8 +31,7 @@ def _test_string(self, cfn, sub_string, parameters, tree):
         """Test if a string has appropriate parameters"""
 
         matches = list()
-        regex = re.compile(r'\${[^!].*?}')
-        string_params = regex.findall(sub_string)
+        string_params = cfn.get_sub_parameters(sub_string)
         get_atts = cfn.get_valid_getatts()
 
         valid_pseudo_params = [
@@ -54,7 +52,6 @@ def _test_string(self, cfn, sub_string, parameters, tree):
             valid_params.append(key)
 
         for string_param in string_params:
-            string_param = string_param[2:-1]
             if isinstance(string_param, (six.string_types, six.text_type)):
                 if string_param not in valid_params:
                     found = False
@@ -66,7 +63,7 @@ def _test_string(self, cfn, sub_string, parameters, tree):
                                   attribute_name == '.'.join(string_param.split('.')[1:])):
                                 found = True
                     if not found:
-                        message = 'String parameter {0} not found in string for {1}'
+                        message = 'Parameter {0} for Fn::Sub not found at {1}'
                         matches.append(RuleMatch(
                             tree, message.format(string_param, '/'.join(map(str, tree)))))
 
diff --git a/src/cfnlint/rules/parameters/Used.py b/src/cfnlint/rules/parameters/Used.py
--- a/src/cfnlint/rules/parameters/Used.py
+++ b/src/cfnlint/rules/parameters/Used.py
@@ -56,12 +56,13 @@ def match(self, cfn):
         subs = list()
         for subtree in subtrees:
             if isinstance(subtree[-1], list):
-                subs.append(subtree[-1][0])
+                subs.extend(cfn.get_sub_parameters(subtree[-1][0]))
             else:
-                subs.append(subtree[-1])
+                subs.extend(cfn.get_sub_parameters(subtree[-1]))
+
         for paramname, _ in cfn.get_parameters().items():
             if paramname not in refs:
-                if not self.isparaminref(subs, paramname):
+                if paramname not in subs:
                     message = 'Parameter {0} not used.'
                     matches.append(RuleMatch(
                         ['Parameters', paramname],


Test patch:
diff --git a/test/fixtures/templates/good/generic.yaml b/test/fixtures/templates/good/generic.yaml
--- a/test/fixtures/templates/good/generic.yaml
+++ b/test/fixtures/templates/good/generic.yaml
@@ -91,7 +91,7 @@ Resources:
       NetworkInterfaces:
         - DeviceIndex: "1"
       UserData: !Sub |
-        yum install ${Package}
+        yum install ${ Package}
   mySnsTopic:
     Type: AWS::SNS::Topic
   MyEC2Instance1:
@@ -151,7 +151,7 @@ Resources:
   IamPipeline:
     Type: "AWS::CloudFormation::Stack"
     Properties:
-      TemplateURL: !Sub 'https://s3.${AWS::Region}.amazonaws.com/ss-vsts-codepipeline-${AWS::Region}/vsts/${AWS::AccountId}/templates/vsts-pipeline/pipeline.yaml'
+      TemplateURL: !Sub 'https://s3.${ AWS::Region}.amazonaws.com/ss-vsts-codepipeline-${AWS::Region}/vsts/${AWS::AccountId}/templates/vsts-pipeline/pipeline.yaml'
       Parameters:
         DeploymentName: iam-pipeline
         Deploy: 'auto'