Repository: serverless/serverless (JavaScript) — Bug Fix

Issue:
Can't parse variable system when a second argument is hard-coded
<!--
1. If you have a question and not a bug/feature request please ask it at http://forum.serverless.com
2. Please check if an issue already exists so there are no duplicates
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->

# This is a Bug Report

## Description

For bug reports:
* What went wrong?

When using a variable system, you can't parse properly a specified value when a second argument is hard-coded.

* What did you expect should have happened?

parsing as expected.

* What was the config you used?

```yaml
provider:
  name: aws
  runtime: nodejs6.10
  environment:
    LAMBDA_SCHEDULE: ${env:LAMBDA_SCHEDULE, 'cron(0 0 * * ? *)'}
functions:
    cronFunc:
        handler: handler.cronFunc
        events:
           - schedule:
              rate: ${self:provider.environment.LAMBDA_SCHEDULE}
              enabled: true
```

Currently, it is transformed like this:

```yaml
"Type": "AWS::Lambda::Function",
      "Properties": {
        "Code": {
          "S3Bucket": {
            "Ref": "ServerlessDeploymentBucket"
          },
          "S3Key": "serverless/aws-pppppp/dev/1514995580830-2018-01-03T16:06:20.830Z/aws-pppppp.zip"
        },
        "FunctionName": "aws-pppppp-dev-cronFunc",
        "Handler": "handler.cronFunc",
        "MemorySize": 1024,
        "Role": {
          "Fn::GetAtt": [
            "IamRoleLambdaExecution",
            "Arn"
          ]
        },
        "Runtime": "nodejs6.10",
        "Timeout": 6,
        "Environment": {
          "Variables": {
            "LAMBDA_SCHEDULE": "${env:LAMBDA_SCHEDULE, 'cron(0 0 * * ? *)'}"
          }
        }
      }
```

* What stacktrace or error message from your provider did you see?
Any errors are not detected.


## Additional Data
* ***Serverless Framework Version you're using***: 1.25.0
* ***Operating System***: darwin
* ***Stack Trace***: none
* ***Provider Error messages***: none



Discussion:
I'm facing this issue as well. It's been open for quite a while - is anyone on it?

Resolving patch:
diff --git a/lib/classes/Service.js b/lib/classes/Service.js
index 1e372ad0631..afd489d1a3f 100644
--- a/lib/classes/Service.js
+++ b/lib/classes/Service.js
@@ -21,7 +21,7 @@ class Service {
     this.serviceObject = null;
     this.provider = {
       stage: 'dev',
-      variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}',
+      variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}',
     };
     this.custom = {};
     this.plugins = [];
diff --git a/lib/classes/Utils.js b/lib/classes/Utils.js
index 38a4a1e7d10..56553f6659c 100644
--- a/lib/classes/Utils.js
+++ b/lib/classes/Utils.js
@@ -276,7 +276,7 @@ class Utils {
       }
 
       let hasCustomVariableSyntaxDefined = false;
-      const defaultVariableSyntax = '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)]+?)}';
+      const defaultVariableSyntax = '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}';
 
       // check if the variableSyntax in the provider section is defined
       if (
diff --git a/lib/plugins/print/print.js b/lib/plugins/print/print.js
index 650d5258823..1c71ae9bebd 100644
--- a/lib/plugins/print/print.js
+++ b/lib/plugins/print/print.js
@@ -54,7 +54,7 @@ class Print {
       {
         stage: 'dev',
         region: 'us-east-1',
-        variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}',
+        variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}',
       },
       service.provider
     );


Test patch:
diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js
index 8d599578aee..3b0b398d10c 100644
--- a/lib/classes/Service.test.js
+++ b/lib/classes/Service.test.js
@@ -31,7 +31,7 @@ describe('Service', () => {
       expect(serviceInstance.serviceObject).to.be.equal(null);
       expect(serviceInstance.provider).to.deep.equal({
         stage: 'dev',
-        variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}',
+        variableSyntax: '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}',
       });
       expect(serviceInstance.custom).to.deep.equal({});
       expect(serviceInstance.plugins).to.deep.equal([]);
@@ -132,7 +132,7 @@ describe('Service', () => {
           name: 'aws',
           stage: 'dev',
           region: 'us-east-1',
-          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}',
+          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
         },
         plugins: ['testPlugin'],
         functions: {
@@ -164,7 +164,7 @@ describe('Service', () => {
         expect(serviceInstance.service).to.be.equal('new-service');
         expect(serviceInstance.provider.name).to.deep.equal('aws');
         expect(serviceInstance.provider.variableSyntax).to.equal(
-          '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}'
+          '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}'
         );
         expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
         expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
@@ -187,7 +187,7 @@ describe('Service', () => {
           name: 'aws',
           stage: 'dev',
           region: 'us-east-1',
-          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}',
+          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
         },
         plugins: ['testPlugin'],
         functions: {
@@ -218,7 +218,7 @@ describe('Service', () => {
         expect(serviceInstance.service).to.be.equal('new-service');
         expect(serviceInstance.provider.name).to.deep.equal('aws');
         expect(serviceInstance.provider.variableSyntax).to.equal(
-          '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}'
+          '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}'
         );
         expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
         expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
@@ -241,7 +241,7 @@ describe('Service', () => {
           name: 'aws',
           stage: 'dev',
           region: 'us-east-1',
-          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}',
+          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
         },
         plugins: ['testPlugin'],
         functions: {
@@ -275,7 +275,7 @@ describe('Service', () => {
         expect(serviceInstance.service).to.be.equal('new-service');
         expect(serviceInstance.provider.name).to.deep.equal('aws');
         expect(serviceInstance.provider.variableSyntax).to.equal(
-          '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}'
+          '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}'
         );
         expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
         expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
@@ -298,7 +298,7 @@ describe('Service', () => {
           name: 'aws',
           stage: 'dev',
           region: 'us-east-1',
-          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}',
+          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
         },
         plugins: ['testPlugin'],
         functions: {
@@ -332,7 +332,7 @@ describe('Service', () => {
         expect(serviceInstance.service).to.be.equal('new-service');
         expect(serviceInstance.provider.name).to.deep.equal('aws');
         expect(serviceInstance.provider.variableSyntax).to.equal(
-          '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}'
+          '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}'
         );
         expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
         expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
@@ -355,7 +355,7 @@ describe('Service', () => {
           name: 'aws',
           stage: 'dev',
           region: 'us-east-1',
-          variableSyntax: '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}',
+          variableSyntax: '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)*?]+?)}}',
         },
         plugins: ['testPlugin'],
         functions: {
@@ -389,7 +389,7 @@ describe('Service', () => {
         expect(serviceInstance.service).to.be.equal('new-service');
         expect(serviceInstance.provider.name).to.deep.equal('aws');
         expect(serviceInstance.provider.variableSyntax).to.equal(
-          '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)]+?)}}'
+          '\\${{([ ~:a-zA-Z0-9._\'",\\-\\/\\(\\)*?]+?)}}'
         );
         expect(serviceInstance.plugins).to.deep.equal(['testPlugin']);
         expect(serviceInstance.resources.aws).to.deep.equal({ resourcesProp: 'value' });
@@ -429,7 +429,7 @@ describe('Service', () => {
           name: 'aws',
           stage: 'dev',
           region: 'us-east-1',
-          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}',
+          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
         },
         plugins: ['testPlugin'],
         functions: {
@@ -997,7 +997,7 @@ describe('Service', () => {
           name: 'aws',
           stage: 'dev',
           region: 'us-east-1',
-          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}',
+          variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}',
         },
         plugins: ['testPlugin'],
         functions: {
diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js
index f52c81f914e..678dbe90f80 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -56,7 +56,7 @@ describe('Variables', () => {
   describe('#loadVariableSyntax()', () => {
     it('should set variableSyntax', () => {
       // eslint-disable-next-line no-template-curly-in-string
-      serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}';
+      serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}';
       serverless.variables.loadVariableSyntax();
       expect(serverless.variables.variableSyntax).to.be.a('RegExp');
     });
@@ -480,7 +480,7 @@ describe('Variables', () => {
       beforeEach(() => {
         service = makeDefault();
         // eslint-disable-next-line no-template-curly-in-string
-        service.provider.variableSyntax = '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}'; // default
+        service.provider.variableSyntax = '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}'; // default
         serverless.variables.service = service;
         serverless.variables.loadVariableSyntax();
         delete service.provider.variableSyntax;
@@ -889,6 +889,24 @@ describe('Variables', () => {
         };
         return serverless.variables.populateObject(service.custom).should.become(expected);
       });
+      it('should preserve question mark in single-quote literal fallback', () => {
+        service.custom = {
+          val0: "${self:custom.val, '?'}",
+        };
+        const expected = {
+          val0: '?',
+        };
+        return serverless.variables.populateObject(service.custom).should.become(expected);
+      });
+      it('should preserve question mark in single-quote literal fallback', () => {
+        service.custom = {
+          val0: "${self:custom.val, 'cron(0 0 * * ? *)'}",
+        };
+        const expected = {
+          val0: 'cron(0 0 * * ? *)',
+        };
+        return serverless.variables.populateObject(service.custom).should.become(expected);
+      });
       it('should accept whitespace in variables', () => {
         service.custom = {
           val0: '${self: custom.val}',
@@ -901,7 +919,7 @@ describe('Variables', () => {
         return serverless.variables.populateObject(service.custom).should.become(expected);
       });
       it('should handle deep variables regardless of custom variableSyntax', () => {
-        service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*]+?)}}';
+        service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*?]+?)}}';
         serverless.variables.loadVariableSyntax();
         delete service.provider.variableSyntax;
         service.custom = {
@@ -917,7 +935,7 @@ describe('Variables', () => {
         return serverless.variables.populateObject(service.custom).should.become(expected);
       });
       it('should handle deep variable continuations regardless of custom variableSyntax', () => {
-        service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)]+?)}}';
+        service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*?]+?)}}';
         serverless.variables.loadVariableSyntax();
         delete service.provider.variableSyntax;
         service.custom = {
@@ -933,7 +951,7 @@ describe('Variables', () => {
         return serverless.variables.populateObject(service.custom).should.become(expected);
       });
       it('should handle deep variables regardless of recursion into custom variableSyntax', () => {
-        service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*]+?)}}';
+        service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*?]+?)}}';
         serverless.variables.loadVariableSyntax();
         delete service.provider.variableSyntax;
         service.custom = {
@@ -953,7 +971,7 @@ describe('Variables', () => {
         return serverless.variables.populateObject(service.custom).should.become(expected);
       });
       it('should handle deep variables in complex recursions of custom variableSyntax', () => {
-        service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*]+?)}}';
+        service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*?]+?)}}';
         serverless.variables.loadVariableSyntax();
         delete service.provider.variableSyntax;
         service.custom = {
@@ -1333,7 +1351,7 @@ module.exports = {
 
   describe('#overwrite()', () => {
     beforeEach(() => {
-      serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}';
+      serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}';
       serverless.variables.loadVariableSyntax();
       delete serverless.service.provider.variableSyntax;
     });
@@ -1622,7 +1640,7 @@ module.exports = {
 
   describe('#getValueFromSelf()', () => {
     beforeEach(() => {
-      serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}}';
+      serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}}';
       serverless.variables.loadVariableSyntax();
       delete serverless.service.provider.variableSyntax;
     });
diff --git a/lib/plugins/print/print.test.js b/lib/plugins/print/print.test.js
index 979e16704bd..c417790a725 100644
--- a/lib/plugins/print/print.test.js
+++ b/lib/plugins/print/print.test.js
@@ -38,7 +38,7 @@ describe('Print', () => {
   });
 
   afterEach(() => {
-    serverless.service.provider.variableSyntax = '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*]+?)}';
+    serverless.service.provider.variableSyntax = '\\${([ ~:a-zA-Z0-9._@\'",\\-\\/\\(\\)*?]+?)}';
   });
 
   it('should print standard config', () => {
@@ -262,10 +262,10 @@ describe('Print', () => {
       provider: {
         name: 'aws',
         stage: '${{opt:stage}}',
-        variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*]+?)}}',
+        variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*?]+?)}}',
       },
     };
-    serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*]+?)}}';
+    serverless.service.provider.variableSyntax = '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*?]+?)}}';
     getServerlessConfigFileStub.resolves(conf);
 
     serverless.processedInput = {
@@ -278,7 +278,7 @@ describe('Print', () => {
       provider: {
         name: 'aws',
         stage: 'dev',
-        variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*]+?)}}',
+        variableSyntax: '\\${{([ ~:a-zA-Z0-9._@\\\'",\\-\\/\\(\\)*?]+?)}}',
       },
     };