Repository: serverless/serverless (JavaScript)
Date: 2017-07-03 12:46:15+00:00
Task type: Bug Fix

Issue:
Variable file() fails with absolute path
# This is a Bug Report

## Description

### What went wrong?
Could not load `file()` when path given was absolute

### What did you expect should have happened?
`file()` should handle absolute paths

### What was the config you used?
```yaml
...
provider:
  environment: ${file(/Users/damon/development/mindhive/graymont-trucks/deploy/build/dev/cloud/env-local.json)}
```

### What stacktrace or error message from your provider did you see?
```
 Serverless Warning --------------------------------------
 
     A valid file to satisfy the declaration 'file(/Users/damon/development/mindhive/graymont-trucks/deploy/build/dev/cloud/env-local.json)'
     could not be found.
```

## Additional Data

Just to show the file from the example above should work, it does exist: 
```
$ ll /Users/damon/development/mindhive/graymont-trucks/deploy/build/dev/cloud/env-local.json
-rw-r--r--  1 damon  staff  560 16 May 20:52 /Users/damon/development/mindhive/graymont-trucks/deploy/build/dev/cloud/env-local.json
```

And if I change the path in serverless.yaml to be relative it works:
```yaml
...
provider:
  environment: ${file(../deploy/build/dev/cloud/env-local.json)}
```

* ***Serverless Framework Version you're using***:
* ***Operating System***:
```
  Your Environment Information -----------------------------
     OS:                 darwin
     Node Version:       7.10.0
     Serverless Version: 1.13.2
```


Issue discussion:

Solution patch:
diff --git a/lib/classes/Service.js b/lib/classes/Service.js
index 02c3364e3b0..9a5636e60bf 100644
--- a/lib/classes/Service.js
+++ b/lib/classes/Service.js
@@ -16,7 +16,7 @@ class Service {
     this.provider = {
       stage: 'dev',
       region: 'us-east-1',
-      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 5a81955cc58..d2854f3470b 100644
--- a/lib/classes/Utils.js
+++ b/lib/classes/Utils.js
@@ -249,7 +249,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 (provider && provider.variableSyntax
diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js
index 6b575609bd3..97aaef67922 100644
--- a/lib/classes/Variables.js
+++ b/lib/classes/Variables.js
@@ -5,6 +5,7 @@ const path = require('path');
 const replaceall = require('replaceall');
 const logWarning = require('./Error').logWarning;
 const BbPromise = require('bluebird');
+const os = require('os');
 
 class Variables {
 
@@ -13,7 +14,7 @@ class Variables {
     this.service = this.serverless.service;
 
     this.overwriteSyntax = RegExp(/,/g);
-    this.fileRefSyntax = RegExp(/^file\(([a-zA-Z0-9._\-/]+?)\)/g);
+    this.fileRefSyntax = RegExp(/^file\((~?[a-zA-Z0-9._\-/]+?)\)/g);
     this.envRefSyntax = RegExp(/^env:/g);
     this.optRefSyntax = RegExp(/^opt:/g);
     this.selfRefSyntax = RegExp(/^self:/g);
@@ -215,12 +216,14 @@ class Variables {
   getValueFromFile(variableString) {
     const matchedFileRefString = variableString.match(this.fileRefSyntax)[0];
     const referencedFileRelativePath = matchedFileRefString
-      .replace(this.fileRefSyntax, (match, varName) => varName.trim());
-    const referencedFileFullPath = path.join(this.serverless.config.servicePath,
-      referencedFileRelativePath);
+      .replace(this.fileRefSyntax, (match, varName) => varName.trim())
+      .replace('~', os.homedir());
+
+    const referencedFileFullPath = (path.isAbsolute(referencedFileRelativePath) ?
+        referencedFileRelativePath :
+        path.join(this.serverless.config.servicePath, referencedFileRelativePath));
     let fileExtension = referencedFileRelativePath.split('.');
     fileExtension = fileExtension[fileExtension.length - 1];
-
     // Validate file exists
     if (!this.serverless.utils.fileExistsSync(referencedFileFullPath)) {
       return BbPromise.resolve(undefined);
@@ -274,7 +277,6 @@ class Variables {
         return this.getDeepValue(deepProperties, valueToPopulate);
       }
     }
-
     return BbPromise.resolve(valueToPopulate);
   }
 


Test patch:
diff --git a/lib/classes/Service.test.js b/lib/classes/Service.test.js
index 22041857deb..4cca7dfa903 100644
--- a/lib/classes/Service.test.js
+++ b/lib/classes/Service.test.js
@@ -31,7 +31,7 @@ describe('Service', () => {
       expect(serviceInstance.provider).to.deep.equal({
         stage: 'dev',
         region: 'us-east-1',
-        variableSyntax: '\\${([ :a-zA-Z0-9._,\\-\\/\\(\\)]+?)}',
+        variableSyntax: '\\${([ ~:a-zA-Z0-9._,\\-\\/\\(\\)]+?)}',
       });
       expect(serviceInstance.custom).to.deep.equal({});
       expect(serviceInstance.plugins).to.deep.equal([]);
diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js
index e06bea7ab75..eb1672a745f 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -12,6 +12,7 @@ const testUtils = require('../../tests/utils');
 const slsError = require('./Error');
 const AwsProvider = require('../plugins/aws/provider/awsProvider');
 const BbPromise = require('bluebird');
+const os = require('os');
 
 describe('Variables', () => {
   describe('#constructor()', () => {
@@ -535,6 +536,33 @@ describe('Variables', () => {
   });
 
   describe('#getValueFromFile()', () => {
+    it('should work for absolute paths with ~ ', () => {
+      const serverless = new Serverless();
+      const expectedFileName = `${os.homedir}/somedir/config.yml`;
+      const configYml = {
+        test: 1,
+        test2: 'test2',
+        testObj: {
+          sub: 2,
+          prob: 'prob',
+        },
+      };
+      const fileExistsStub = sinon
+        .stub(serverless.utils, 'fileExistsSync').returns(true);
+
+      const readFileSyncStub = sinon
+        .stub(serverless.utils, 'readFileSync').returns(configYml);
+
+      return serverless.variables.getValueFromFile('file(~/somedir/config.yml)')
+        .then(valueToPopulate => {
+          expect(fileExistsStub.calledWithMatch(expectedFileName));
+          expect(readFileSyncStub.calledWithMatch(expectedFileName));
+          expect(valueToPopulate).to.deep.equal(configYml);
+          readFileSyncStub.restore();
+          fileExistsStub.restore();
+        });
+    });
+
     it('should populate an entire variable file', () => {
       const serverless = new Serverless();
       const SUtils = new Utils();