From 96c0e0e8bcc8d2421b0437ad7b64d1f372a41092 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Tue, 25 Jan 2022 21:50:36 -0800 Subject: [PATCH 01/10] Simplify parse --- lib/main2.js | 47 ++++++++++++++++++++++++++++ tests/.env2 | 26 ++++++++++++++++ tests/test-parse2.js | 73 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+) create mode 100644 lib/main2.js create mode 100644 tests/.env2 create mode 100644 tests/test-parse2.js diff --git a/lib/main2.js b/lib/main2.js new file mode 100644 index 00000000..d6b18edd --- /dev/null +++ b/lib/main2.js @@ -0,0 +1,47 @@ +const LINE = /(?:^|\A)\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|[^\#\r\n]+)?\s*(?:\#.*)?(?:$|\z)/mg // eslint-disable-line + +// Parser src into an Object +function parse (src) { + const obj = {} + + // Convert buffer to string + let lines = src.toString() + + // Convert line breaks to same format + lines = lines.replace(/\r\n?/, '\n') + + let match + while ((match = LINE.exec(lines)) != null) { + const key = match[1] + + // Default undefined or null to empty string + let value = (match[2] || '') + + value = _parseValue(value) + + obj[key] = value + } + + return obj +} + +function _parseValue (value) { + // Remove whitespace + value = value.trim() + + // Check if double quoted + const maybeQuote = value[0] + + // Remove surrounding quotes + value = value.replace(/^(['"])(.*)\1$/m, '$2') + + // Expand newlines if double quoted + if (maybeQuote === '"') { + value = value.replace(/\\n/g, '\n') + value = value.replace(/\\r/g, '\r') + } + + return value +} + +module.exports.parse = parse diff --git a/tests/.env2 b/tests/.env2 new file mode 100644 index 00000000..8d227b35 --- /dev/null +++ b/tests/.env2 @@ -0,0 +1,26 @@ +BASIC=basic + +# previous line intentionally left blank +AFTER_LINE=after_line +EMPTY= +SINGLE_QUOTES='single_quotes' +SINGLE_QUOTES_SPACED=' single quotes ' +DOUBLE_QUOTES="double_quotes" +DOUBLE_QUOTES_SPACED=" double quotes " +DOUBLE_QUOTES_INSIDE_SINGLE='double "quotes" work inside single quotes' +SINGLE_QUOTES_INSIDE_DOUBLE="single 'quotes' work inside double quotes" +EXPAND_NEWLINES="expand\nnew\nlines" +DONT_EXPAND_UNQUOTED=dontexpand\nnewlines +DONT_EXPAND_SQUOTED='dontexpand\nnewlines' +# COMMENTS=work +INLINE_COMMENTS=inline comments # work #very #well +INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work +INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work +INLINE_COMMENTS_SPACE=inline comments dont have to start with a#space +EQUAL_SIGNS=equals== +RETAIN_INNER_QUOTES={"foo": "bar"} +RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}' +TRIM_SPACE_FROM_UNQUOTED= some spaced out string +USERNAME=therealnerdybeast@example.tld + SPACED_KEY = parsed +UNQUOTED_ESCAPE_CHARACTERS=bar\\ bar diff --git a/tests/test-parse2.js b/tests/test-parse2.js new file mode 100644 index 00000000..76b88ad3 --- /dev/null +++ b/tests/test-parse2.js @@ -0,0 +1,73 @@ +const fs = require('fs') + +const t = require('tap') + +const dotenv = require('../lib/main2') + +const parsed = dotenv.parse(fs.readFileSync('tests/.env2', { encoding: 'utf8' })) + +t.type(parsed, Object, 'should return an object') + +t.equal(parsed.BASIC, 'basic', 'sets basic environment variable') + +t.equal(parsed.AFTER_LINE, 'after_line', 'reads after a skipped line') + +t.equal(parsed.EMPTY, '', 'defaults empty values to empty string') + +t.equal(parsed.SINGLE_QUOTES, 'single_quotes', 'escapes single quoted values') + +t.equal(parsed.SINGLE_QUOTES_SPACED, ' single quotes ', 'respects surrounding spaces in single quotes') + +t.equal(parsed.DOUBLE_QUOTES, 'double_quotes', 'escapes double quoted values') + +t.equal(parsed.DOUBLE_QUOTES_SPACED, ' double quotes ', 'respects surrounding spaces in double quotes') + +t.equal(parsed.DOUBLE_QUOTES_INSIDE_SINGLE, 'double "quotes" work inside single quotes', 'respects double quotes inside single quotes') + +t.equal(parsed.SINGLE_QUOTES_INSIDE_DOUBLE, "single 'quotes' work inside double quotes", 'respects single quotes inside double quotes') + +t.equal(parsed.EXPAND_NEWLINES, 'expand\nnew\nlines', 'expands newlines but only if double quoted') + +t.equal(parsed.DONT_EXPAND_UNQUOTED, 'dontexpand\\nnewlines', 'expands newlines but only if double quoted') + +t.equal(parsed.DONT_EXPAND_SQUOTED, 'dontexpand\\nnewlines', 'expands newlines but only if double quoted') + +t.notOk(parsed.COMMENTS, 'ignores commented lines') + +t.equal(parsed.INLINE_COMMENTS, 'inline comments', 'ignores inline comments') + +t.equal(parsed.INLINE_COMMENTS_SINGLE_QUOTES, 'inline comments outside of #singlequotes', 'ignores inline comments, but respects # character inside of single quotes') + +t.equal(parsed.INLINE_COMMENTS_DOUBLE_QUOTES, 'inline comments outside of #doublequotes', 'ignores inline comments, but respects # character inside of double quotes') + +t.equal(parsed.INLINE_COMMENTS_SPACE, 'inline comments dont have to start with a', 'respects # character in values when it is not preceded by a space character') + +t.equal(parsed.EQUAL_SIGNS, 'equals==', 'respects equals signs in values') + +t.equal(parsed.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes') + +t.equal(parsed.EQUAL_SIGNS, 'equals==', 'respects equals signs in values') + +t.equal(parsed.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes') + +t.equal(parsed.RETAIN_INNER_QUOTES_AS_STRING, '{"foo": "bar"}', 'retains inner quotes') + +t.equal(parsed.TRIM_SPACE_FROM_UNQUOTED, 'some spaced out string', 'retains spaces in string') + +t.equal(parsed.USERNAME, 'therealnerdybeast@example.tld', 'parses email addresses completely') + +t.equal(parsed.SPACED_KEY, 'parsed', 'parses keys and values surrounded by spaces') + +const payload = dotenv.parse(Buffer.from('BUFFER=true')) +t.equal(payload.BUFFER, 'true', 'should parse a buffer into an object') + +const expectedPayload = { SERVER: 'localhost', PASSWORD: 'password', DB: 'tests' } + +const RPayload = dotenv.parse(Buffer.from('SERVER=localhost\rPASSWORD=password\rDB=tests\r')) +t.same(RPayload, expectedPayload, 'can parse (\\r) line endings') + +const NPayload = dotenv.parse(Buffer.from('SERVER=localhost\nPASSWORD=password\nDB=tests\n')) +t.same(NPayload, expectedPayload, 'can parse (\\n) line endings') + +const RNPayload = dotenv.parse(Buffer.from('SERVER=localhost\r\nPASSWORD=password\r\nDB=tests\r\n')) +t.same(RNPayload, expectedPayload, 'can parse (\\r\\n) line endings') From dbb50ee902820cc0a188ab7a60ea08645372b293 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Tue, 25 Jan 2022 23:22:53 -0800 Subject: [PATCH 02/10] Add main2.js - to replace main.js --- lib/main2.js | 38 ++++++++++----------- tests/.env-multiline2 | 32 ++++++++++++++++++ tests/test-parse-multiline.js | 2 -- tests/test-parse2-multiline.js | 60 ++++++++++++++++++++++++++++++++++ tests/test-parse2.js | 1 - 5 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 tests/.env-multiline2 create mode 100644 tests/test-parse2-multiline.js diff --git a/lib/main2.js b/lib/main2.js index d6b18edd..2d10d3f9 100644 --- a/lib/main2.js +++ b/lib/main2.js @@ -1,4 +1,4 @@ -const LINE = /(?:^|\A)\s*(?:export\s+)?([\w\.]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|[^\#\r\n]+)?\s*(?:\#.*)?(?:$|\z)/mg // eslint-disable-line +const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg // Parser src into an Object function parse (src) { @@ -8,7 +8,7 @@ function parse (src) { let lines = src.toString() // Convert line breaks to same format - lines = lines.replace(/\r\n?/, '\n') + lines = lines.replace(/\r\n?/mg, '\n') let match while ((match = LINE.exec(lines)) != null) { @@ -17,31 +17,27 @@ function parse (src) { // Default undefined or null to empty string let value = (match[2] || '') - value = _parseValue(value) + // Remove whitespace + value = value.trim() - obj[key] = value - } - - return obj -} - -function _parseValue (value) { - // Remove whitespace - value = value.trim() + // Check if double quoted + const maybeQuote = value[0] - // Check if double quoted - const maybeQuote = value[0] + // Remove surrounding quotes + // value = value.replace(/^(['"])(.*)\1$/mg, '$2') + value = value.replace(/^(['"])([\s\S]+)\1$/mg, '$2') - // Remove surrounding quotes - value = value.replace(/^(['"])(.*)\1$/m, '$2') + // Expand newlines if double quoted + if (maybeQuote === '"') { + value = value.replace(/\\n/g, '\n') + value = value.replace(/\\r/g, '\r') + } - // Expand newlines if double quoted - if (maybeQuote === '"') { - value = value.replace(/\\n/g, '\n') - value = value.replace(/\\r/g, '\r') + // Add to object + obj[key] = value } - return value + return obj } module.exports.parse = parse diff --git a/tests/.env-multiline2 b/tests/.env-multiline2 new file mode 100644 index 00000000..758b3377 --- /dev/null +++ b/tests/.env-multiline2 @@ -0,0 +1,32 @@ +BASIC=basic + +# previous line intentionally left blank +AFTER_LINE=after_line +EMPTY= +SINGLE_QUOTES='single_quotes' +SINGLE_QUOTES_SPACED=' single quotes ' +DOUBLE_QUOTES="double_quotes" +DOUBLE_QUOTES_SPACED=" double quotes " +EXPAND_NEWLINES="expand\nnew\nlines" +DONT_EXPAND_UNQUOTED=dontexpand\nnewlines +DONT_EXPAND_SQUOTED='dontexpand\nnewlines' +# COMMENTS=work +EQUAL_SIGNS=equals== +RETAIN_INNER_QUOTES={"foo": "bar"} + +RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}' +TRIM_SPACE_FROM_UNQUOTED= some spaced out string +USERNAME=therealnerdybeast@example.tld + SPACED_KEY = parsed + +MULTI_DOUBLE_QUOTED="THIS +IS +A +MULTILINE +STRING" + +MULTI_SINGLE_QUOTED='THIS +IS +A +MULTILINE +STRING' diff --git a/tests/test-parse-multiline.js b/tests/test-parse-multiline.js index 7ece51ae..37166da3 100644 --- a/tests/test-parse-multiline.js +++ b/tests/test-parse-multiline.js @@ -1,5 +1,3 @@ -/* @flow */ - const fs = require('fs') const sinon = require('sinon') diff --git a/tests/test-parse2-multiline.js b/tests/test-parse2-multiline.js new file mode 100644 index 00000000..aff13b05 --- /dev/null +++ b/tests/test-parse2-multiline.js @@ -0,0 +1,60 @@ +const fs = require('fs') +const t = require('tap') + +const dotenv = require('../lib/main2') + +const parsed = dotenv.parse(fs.readFileSync('tests/.env-multiline2', { encoding: 'utf8' }), { multiline: 'true' }) + +t.type(parsed, Object, 'should return an object') + +t.equal(parsed.BASIC, 'basic', 'sets basic environment variable') + +t.equal(parsed.AFTER_LINE, 'after_line', 'reads after a skipped line') + +t.equal(parsed.EMPTY, '', 'defaults empty values to empty string') + +t.equal(parsed.SINGLE_QUOTES, 'single_quotes', 'escapes single quoted values') + +t.equal(parsed.SINGLE_QUOTES_SPACED, ' single quotes ', 'respects surrounding spaces in single quotes') + +t.equal(parsed.DOUBLE_QUOTES, 'double_quotes', 'escapes double quoted values') + +t.equal(parsed.DOUBLE_QUOTES_SPACED, ' double quotes ', 'respects surrounding spaces in double quotes') + +t.equal(parsed.EXPAND_NEWLINES, 'expand\nnew\nlines', 'expands newlines but only if double quoted') + +t.equal(parsed.DONT_EXPAND_UNQUOTED, 'dontexpand\\nnewlines', 'expands newlines but only if double quoted') + +t.equal(parsed.DONT_EXPAND_SQUOTED, 'dontexpand\\nnewlines', 'expands newlines but only if double quoted') + +t.notOk(parsed.COMMENTS, 'ignores commented lines') + +t.equal(parsed.EQUAL_SIGNS, 'equals==', 'respects equals signs in values') + +t.equal(parsed.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes') + +t.equal(parsed.RETAIN_INNER_QUOTES_AS_STRING, '{"foo": "bar"}', 'retains inner quotes') + +t.equal(parsed.TRIM_SPACE_FROM_UNQUOTED, 'some spaced out string', 'retains spaces in string') + +t.equal(parsed.USERNAME, 'therealnerdybeast@example.tld', 'parses email addresses completely') + +t.equal(parsed.SPACED_KEY, 'parsed', 'parses keys and values surrounded by spaces') + +t.equal(parsed.MULTI_DOUBLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING', 'parses multi-line strings when using double quotes') + +t.equal(parsed.MULTI_SINGLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING', 'parses multi-line strings when using single quotes') + +const payload = dotenv.parse(Buffer.from('BUFFER=true')) +t.equal(payload.BUFFER, 'true', 'should parse a buffer into an object') + +const expectedPayload = { SERVER: 'localhost', PASSWORD: 'password', DB: 'tests' } + +const RPayload = dotenv.parse(Buffer.from('SERVER=localhost\rPASSWORD=password\rDB=tests\r')) +t.same(RPayload, expectedPayload, 'can parse (\\r) line endings') + +const NPayload = dotenv.parse(Buffer.from('SERVER=localhost\nPASSWORD=password\nDB=tests\n')) +t.same(NPayload, expectedPayload, 'can parse (\\n) line endings') + +const RNPayload = dotenv.parse(Buffer.from('SERVER=localhost\r\nPASSWORD=password\r\nDB=tests\r\n')) +t.same(RNPayload, expectedPayload, 'can parse (\\r\\n) line endings') diff --git a/tests/test-parse2.js b/tests/test-parse2.js index 76b88ad3..79ad64f1 100644 --- a/tests/test-parse2.js +++ b/tests/test-parse2.js @@ -1,5 +1,4 @@ const fs = require('fs') - const t = require('tap') const dotenv = require('../lib/main2') From e96ca3e48e577822100c97f396e6f253dc10cf3d Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Tue, 25 Jan 2022 23:31:10 -0800 Subject: [PATCH 03/10] Support multiline by default - no option required --- README.md | 40 -------- lib/main2.js | 69 ++++++++++++- tests/test-config2.js | 171 +++++++++++++++++++++++++++++++++ tests/test-parse2-multiline.js | 2 +- 4 files changed, 240 insertions(+), 42 deletions(-) create mode 100644 tests/test-config2.js diff --git a/README.md b/README.md index 076b512a..138df1bd 100644 --- a/README.md +++ b/README.md @@ -163,27 +163,6 @@ Override any environment variables that have already been set on your machine wi require('dotenv').config({ override: true }) ``` -##### Multiline - -Default: `false` - -Turn on multiline line break parsing. - -```js -require('dotenv').config({ multiline: true }) -``` - -This allows specifying multiline values in this format: - -``` -PRIVATE_KEY="-----BEGIN PRIVATE KEY----- -MIGT... -7ure... ------END PRIVATE KEY-----" -``` - -Ensure that the value begins with a single or double quote character, and it ends with the same character. - ### Parse The engine which parses the contents of your file containing environment @@ -213,25 +192,6 @@ const config = dotenv.parse(buf, opt) // expect a debug message because the buffer is not in KEY=VAL form ``` -##### Multiline - -Default: `false` - -Turn on multiline line break parsing. - -```js -require('dotenv').config({ multiline: true }) -``` - -This allows specifying multiline values in this format: - -``` -PRIVATE_KEY="-----BEGIN PRIVATE KEY----- -MIGT... -7ure... ------END PRIVATE KEY-----" -``` - ## Other Usage ### Preload diff --git a/lib/main2.js b/lib/main2.js index 2d10d3f9..eef02afd 100644 --- a/lib/main2.js +++ b/lib/main2.js @@ -1,3 +1,7 @@ +const fs = require('fs') +const path = require('path') +const os = require('os') + const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg // Parser src into an Object @@ -40,4 +44,67 @@ function parse (src) { return obj } -module.exports.parse = parse +function _log (message) { + console.log(`[dotenv][DEBUG] ${message}`) +} + +function _resolveHome (envPath) { + return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath +} + +// Populates process.env from .env file +function config (options) { + let dotenvPath = path.resolve(process.cwd(), '.env') + let encoding = 'utf8' + const debug = Boolean(options && options.debug) + const override = Boolean(options && options.override) + + if (options) { + if (options.path != null) { + dotenvPath = _resolveHome(options.path) + } + if (options.encoding != null) { + encoding = options.encoding + } + } + + try { + // Specifying an encoding returns a string instead of a buffer + const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding })) + + Object.keys(parsed).forEach(function (key) { + if (!Object.prototype.hasOwnProperty.call(process.env, key)) { + process.env[key] = parsed[key] + } else { + if (override === true) { + process.env[key] = parsed[key] + } + + if (debug) { + if (override === true) { + _log(`"${key}" is already defined in \`process.env\` and WAS overwritten`) + } else { + _log(`"${key}" is already defined in \`process.env\` and was NOT overwritten`) + } + } + } + }) + + return { parsed } + } catch (e) { + if (debug) { + _log(`Failed to load ${dotenvPath} ${e.message}`) + } + + return { error: e } + } +} + +const DotenvModule = { + config, + parse +} + +module.exports.config = DotenvModule.config +module.exports.parse = DotenvModule.parse +module.exports = DotenvModule diff --git a/tests/test-config2.js b/tests/test-config2.js new file mode 100644 index 00000000..d3731aba --- /dev/null +++ b/tests/test-config2.js @@ -0,0 +1,171 @@ +const fs = require('fs') +const os = require('os') +const path = require('path') + +const sinon = require('sinon') +const t = require('tap') + +const dotenv = require('../lib/main2') + +const mockParseResponse = { test: 'foo' } +let readFileSyncStub +let parseStub + +t.beforeEach(() => { + readFileSyncStub = sinon.stub(fs, 'readFileSync').returns('test=foo') + parseStub = sinon.stub(dotenv, 'parse').returns(mockParseResponse) +}) + +t.afterEach(() => { + readFileSyncStub.restore() + parseStub.restore() +}) + +t.test('takes option for path', ct => { + ct.plan(1) + + const testPath = 'tests/.env' + dotenv.config({ path: testPath }) + + ct.equal(readFileSyncStub.args[0][0], testPath) +}) + +t.test('takes option for path along with home directory char ~', ct => { + ct.plan(2) + const mockedHomedir = '/Users/dummy' + const homedirStub = sinon.stub(os, 'homedir').returns(mockedHomedir) + const testPath = '~/.env' + dotenv.config({ path: testPath }) + + ct.equal(readFileSyncStub.args[0][0], path.join(mockedHomedir, '.env')) + ct.ok(homedirStub.called) + homedirStub.restore() +}) + +t.test('takes option for encoding', ct => { + ct.plan(1) + + const testEncoding = 'latin1' + dotenv.config({ encoding: testEncoding }) + + ct.equal(readFileSyncStub.args[0][1].encoding, testEncoding) +}) + +t.test('takes option for debug', ct => { + ct.plan(1) + + const logStub = sinon.stub(console, 'log') + dotenv.config({ debug: 'true' }) + + ct.ok(logStub.called) + logStub.restore() +}) + +t.test('reads path with encoding, parsing output to process.env', ct => { + ct.plan(2) + + const res = dotenv.config() + + ct.same(res.parsed, mockParseResponse) + ct.equal(readFileSyncStub.callCount, 1) +}) + +t.test('does not write over keys already in process.env', ct => { + ct.plan(2) + + const existing = 'bar' + process.env.test = existing + // 'foo' returned as value in `beforeEach`. should keep this 'bar' + const env = dotenv.config() + + ct.equal(env.parsed && env.parsed.test, mockParseResponse.test) + ct.equal(process.env.test, existing) +}) + +t.test('does write over keys already in process.env if override turned on', ct => { + ct.plan(2) + + const existing = 'bar' + process.env.test = existing + // 'foo' returned as value in `beforeEach`. should keep this 'bar' + const env = dotenv.config({ override: true }) + + ct.equal(env.parsed && env.parsed.test, mockParseResponse.test) + ct.equal(process.env.test, 'foo') +}) + +t.test( + 'does not write over keys already in process.env if the key has a falsy value', + ct => { + ct.plan(2) + + const existing = '' + process.env.test = existing + // 'foo' returned as value in `beforeEach`. should keep this '' + const env = dotenv.config() + + ct.equal(env.parsed && env.parsed.test, mockParseResponse.test) + // NB: process.env.test becomes undefined on Windows + ct.notOk(process.env.test) + } +) + +t.test( + 'does write over keys already in process.env if the key has a falsy value but override is set to true', + ct => { + ct.plan(2) + + const existing = '' + process.env.test = existing + // 'foo' returned as value in `beforeEach`. should keep this '' + const env = dotenv.config({ override: true }) + + ct.equal(env.parsed && env.parsed.test, mockParseResponse.test) + // NB: process.env.test becomes undefined on Windows + ct.ok(process.env.test) + } +) + +t.test('returns parsed object', ct => { + ct.plan(2) + + const env = dotenv.config() + + ct.notOk(env.error) + ct.same(env.parsed, mockParseResponse) +}) + +t.test('returns any errors thrown from reading file or parsing', ct => { + ct.plan(1) + + readFileSyncStub.throws() + const env = dotenv.config() + + ct.type(env.error, Error) +}) + +t.test('logs any errors thrown from reading file or parsing when in debug mode', ct => { + ct.plan(2) + + const logStub = sinon.stub(console, 'log') + + readFileSyncStub.throws() + const env = dotenv.config({ debug: true }) + + ct.ok(logStub.called) + ct.type(env.error, Error) + + logStub.restore() +}) + +t.test('logs any errors parsing when in debug and override mode', ct => { + ct.plan(1) + + const logStub = sinon.stub(console, 'log') + + dotenv.config({ debug: true, override: true }) + + ct.ok(logStub.called) + + logStub.restore() +}) diff --git a/tests/test-parse2-multiline.js b/tests/test-parse2-multiline.js index aff13b05..30ff53c5 100644 --- a/tests/test-parse2-multiline.js +++ b/tests/test-parse2-multiline.js @@ -3,7 +3,7 @@ const t = require('tap') const dotenv = require('../lib/main2') -const parsed = dotenv.parse(fs.readFileSync('tests/.env-multiline2', { encoding: 'utf8' }), { multiline: 'true' }) +const parsed = dotenv.parse(fs.readFileSync('tests/.env-multiline2', { encoding: 'utf8' })) t.type(parsed, Object, 'should return an object') From 3abdddb38bad2a5480efd119fcaa88849d23f47f Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Tue, 25 Jan 2022 23:35:18 -0800 Subject: [PATCH 04/10] Move main2 to main --- lib/main.backup.js | 142 ++++++++++++++++++ lib/main.d.ts | 29 +--- lib/main.js | 114 +++++--------- lib/main2.js | 110 -------------- tests/.env | 7 +- tests/.env-multiline | 4 - ...{.env-multiline2 => .env-multiline.backup} | 4 + tests/{.env2 => .env.backup} | 7 +- tests/test-config.js | 9 -- ...{test-config2.js => test-config.js.backup} | 11 +- tests/test-parse-multiline.js | 14 +- ...line.js => test-parse-multiline.js.backup} | 16 +- tests/test-parse.js | 32 +--- .../{test-parse2.js => test-parse.js.backup} | 36 ++++- tests/types/test.ts | 14 +- 15 files changed, 256 insertions(+), 293 deletions(-) create mode 100644 lib/main.backup.js delete mode 100644 lib/main2.js rename tests/{.env-multiline2 => .env-multiline.backup} (94%) rename tests/{.env2 => .env.backup} (83%) rename tests/{test-config2.js => test-config.js.backup} (94%) rename tests/{test-parse2-multiline.js => test-parse-multiline.js.backup} (86%) rename tests/{test-parse2.js => test-parse.js.backup} (70%) diff --git a/lib/main.backup.js b/lib/main.backup.js new file mode 100644 index 00000000..872b5a79 --- /dev/null +++ b/lib/main.backup.js @@ -0,0 +1,142 @@ +const fs = require('fs') +const path = require('path') +const os = require('os') + +function log (message) { + console.log(`[dotenv][DEBUG] ${message}`) +} + +const NEWLINE = '\n' +const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*("[^"]*"|'[^']*'|.*?)(\s+#.*)?$/ +const RE_NEWLINES = /\\n/g +const NEWLINES_MATCH = /\r\n|\n|\r/ + +// Parses src into an Object +function parse (src, options) { + const debug = Boolean(options && options.debug) + const multiline = Boolean(options && options.multiline) + const obj = {} + + // convert Buffers before splitting into lines and processing + const lines = src.toString().split(NEWLINES_MATCH) + + for (let idx = 0; idx < lines.length; idx++) { + let line = lines[idx] + + // matching "KEY' and 'VAL' in 'KEY=VAL' + const keyValueArr = line.match(RE_INI_KEY_VAL) + // matched? + if (keyValueArr != null) { + const key = keyValueArr[1] + // default undefined or missing values to empty string + let val = (keyValueArr[2] || '') + let end = val.length - 1 + const isDoubleQuoted = val[0] === '"' && val[end] === '"' + const isSingleQuoted = val[0] === "'" && val[end] === "'" + + const isMultilineDoubleQuoted = val[0] === '"' && val[end] !== '"' + const isMultilineSingleQuoted = val[0] === "'" && val[end] !== "'" + + // if parsing line breaks and the value starts with a quote + if (multiline && (isMultilineDoubleQuoted || isMultilineSingleQuoted)) { + const quoteChar = isMultilineDoubleQuoted ? '"' : "'" + + val = val.substring(1) + + while (idx++ < lines.length - 1) { + line = lines[idx] + end = line.length - 1 + if (line[end] === quoteChar) { + val += NEWLINE + line.substring(0, end) + break + } + val += NEWLINE + line + } + // if single or double quoted, remove quotes + } else if (isSingleQuoted || isDoubleQuoted) { + val = val.substring(1, end) + + // if double quoted, expand newlines + if (isDoubleQuoted) { + val = val.replace(RE_NEWLINES, NEWLINE) + } + } else { + // remove surrounding whitespace + val = val.trim() + } + + obj[key] = val + } else if (debug) { + const trimmedLine = line.trim() + + // ignore empty and commented lines + if (trimmedLine.length && trimmedLine[0] !== '#') { + log(`Failed to match key and value when parsing line ${idx + 1}: ${line}`) + } + } + } + + return obj +} + +function resolveHome (envPath) { + return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath +} + +// Populates process.env from .env file +function config (options) { + let dotenvPath = path.resolve(process.cwd(), '.env') + let encoding = 'utf8' + const debug = Boolean(options && options.debug) + const override = Boolean(options && options.override) + const multiline = Boolean(options && options.multiline) + + if (options) { + if (options.path != null) { + dotenvPath = resolveHome(options.path) + } + if (options.encoding != null) { + encoding = options.encoding + } + } + + try { + // specifying an encoding returns a string instead of a buffer + const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding }), { debug, multiline }) + + Object.keys(parsed).forEach(function (key) { + if (!Object.prototype.hasOwnProperty.call(process.env, key)) { + process.env[key] = parsed[key] + } else { + if (override === true) { + process.env[key] = parsed[key] + } + + if (debug) { + if (override === true) { + log(`"${key}" is already defined in \`process.env\` and WAS overwritten`) + } else { + log(`"${key}" is already defined in \`process.env\` and was NOT overwritten`) + } + } + } + }) + + return { parsed } + } catch (e) { + if (debug) { + log(`Failed to load ${dotenvPath} ${e.message}`) + } + + return { error: e } + } +} + +const DotenvModule = { + config, + parse +} + +module.exports.config = DotenvModule.config +module.exports.parse = DotenvModule.parse +module.exports = DotenvModule diff --git a/lib/main.d.ts b/lib/main.d.ts index 8bd9b689..63c64a50 100644 --- a/lib/main.d.ts +++ b/lib/main.d.ts @@ -1,32 +1,6 @@ // TypeScript Version: 3.0 /// -export interface DotenvParseOptions { - /** - * Default: `false` - * - * Turn on logging to help debug why certain keys or values are not being set as you expect. - * - * example: `dotenv.parse('KEY=value', { debug: true })` - */ - debug?: boolean; - - /** - * Default: `false` - * - * Turn on multiline line break parsing. - * - * example: - * - * MY_VAR="this - * is - * a - * multiline - * string" - */ - multiline?: boolean; -} - export interface DotenvParseOutput { [name: string]: string; } @@ -41,8 +15,7 @@ export interface DotenvParseOutput { * @returns an object with keys and values based on `src`. example: `{ DB_HOST : 'localhost' }` */ export function parse( - src: string | Buffer, - options?: DotenvParseOptions + src: string | Buffer ): T; export interface DotenvConfigOptions { diff --git a/lib/main.js b/lib/main.js index 872b5a79..eef02afd 100644 --- a/lib/main.js +++ b/lib/main.js @@ -2,84 +2,53 @@ const fs = require('fs') const path = require('path') const os = require('os') -function log (message) { - console.log(`[dotenv][DEBUG] ${message}`) -} - -const NEWLINE = '\n' -const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*("[^"]*"|'[^']*'|.*?)(\s+#.*)?$/ -const RE_NEWLINES = /\\n/g -const NEWLINES_MATCH = /\r\n|\n|\r/ +const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg -// Parses src into an Object -function parse (src, options) { - const debug = Boolean(options && options.debug) - const multiline = Boolean(options && options.multiline) +// Parser src into an Object +function parse (src) { const obj = {} - // convert Buffers before splitting into lines and processing - const lines = src.toString().split(NEWLINES_MATCH) - - for (let idx = 0; idx < lines.length; idx++) { - let line = lines[idx] - - // matching "KEY' and 'VAL' in 'KEY=VAL' - const keyValueArr = line.match(RE_INI_KEY_VAL) - // matched? - if (keyValueArr != null) { - const key = keyValueArr[1] - // default undefined or missing values to empty string - let val = (keyValueArr[2] || '') - let end = val.length - 1 - const isDoubleQuoted = val[0] === '"' && val[end] === '"' - const isSingleQuoted = val[0] === "'" && val[end] === "'" - - const isMultilineDoubleQuoted = val[0] === '"' && val[end] !== '"' - const isMultilineSingleQuoted = val[0] === "'" && val[end] !== "'" - - // if parsing line breaks and the value starts with a quote - if (multiline && (isMultilineDoubleQuoted || isMultilineSingleQuoted)) { - const quoteChar = isMultilineDoubleQuoted ? '"' : "'" - - val = val.substring(1) - - while (idx++ < lines.length - 1) { - line = lines[idx] - end = line.length - 1 - if (line[end] === quoteChar) { - val += NEWLINE + line.substring(0, end) - break - } - val += NEWLINE + line - } - // if single or double quoted, remove quotes - } else if (isSingleQuoted || isDoubleQuoted) { - val = val.substring(1, end) + // Convert buffer to string + let lines = src.toString() - // if double quoted, expand newlines - if (isDoubleQuoted) { - val = val.replace(RE_NEWLINES, NEWLINE) - } - } else { - // remove surrounding whitespace - val = val.trim() - } + // Convert line breaks to same format + lines = lines.replace(/\r\n?/mg, '\n') - obj[key] = val - } else if (debug) { - const trimmedLine = line.trim() + let match + while ((match = LINE.exec(lines)) != null) { + const key = match[1] - // ignore empty and commented lines - if (trimmedLine.length && trimmedLine[0] !== '#') { - log(`Failed to match key and value when parsing line ${idx + 1}: ${line}`) - } + // Default undefined or null to empty string + let value = (match[2] || '') + + // Remove whitespace + value = value.trim() + + // Check if double quoted + const maybeQuote = value[0] + + // Remove surrounding quotes + // value = value.replace(/^(['"])(.*)\1$/mg, '$2') + value = value.replace(/^(['"])([\s\S]+)\1$/mg, '$2') + + // Expand newlines if double quoted + if (maybeQuote === '"') { + value = value.replace(/\\n/g, '\n') + value = value.replace(/\\r/g, '\r') } + + // Add to object + obj[key] = value } return obj } -function resolveHome (envPath) { +function _log (message) { + console.log(`[dotenv][DEBUG] ${message}`) +} + +function _resolveHome (envPath) { return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath } @@ -89,11 +58,10 @@ function config (options) { let encoding = 'utf8' const debug = Boolean(options && options.debug) const override = Boolean(options && options.override) - const multiline = Boolean(options && options.multiline) if (options) { if (options.path != null) { - dotenvPath = resolveHome(options.path) + dotenvPath = _resolveHome(options.path) } if (options.encoding != null) { encoding = options.encoding @@ -101,8 +69,8 @@ function config (options) { } try { - // specifying an encoding returns a string instead of a buffer - const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding }), { debug, multiline }) + // Specifying an encoding returns a string instead of a buffer + const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding })) Object.keys(parsed).forEach(function (key) { if (!Object.prototype.hasOwnProperty.call(process.env, key)) { @@ -114,9 +82,9 @@ function config (options) { if (debug) { if (override === true) { - log(`"${key}" is already defined in \`process.env\` and WAS overwritten`) + _log(`"${key}" is already defined in \`process.env\` and WAS overwritten`) } else { - log(`"${key}" is already defined in \`process.env\` and was NOT overwritten`) + _log(`"${key}" is already defined in \`process.env\` and was NOT overwritten`) } } } @@ -125,7 +93,7 @@ function config (options) { return { parsed } } catch (e) { if (debug) { - log(`Failed to load ${dotenvPath} ${e.message}`) + _log(`Failed to load ${dotenvPath} ${e.message}`) } return { error: e } diff --git a/lib/main2.js b/lib/main2.js deleted file mode 100644 index eef02afd..00000000 --- a/lib/main2.js +++ /dev/null @@ -1,110 +0,0 @@ -const fs = require('fs') -const path = require('path') -const os = require('os') - -const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg - -// Parser src into an Object -function parse (src) { - const obj = {} - - // Convert buffer to string - let lines = src.toString() - - // Convert line breaks to same format - lines = lines.replace(/\r\n?/mg, '\n') - - let match - while ((match = LINE.exec(lines)) != null) { - const key = match[1] - - // Default undefined or null to empty string - let value = (match[2] || '') - - // Remove whitespace - value = value.trim() - - // Check if double quoted - const maybeQuote = value[0] - - // Remove surrounding quotes - // value = value.replace(/^(['"])(.*)\1$/mg, '$2') - value = value.replace(/^(['"])([\s\S]+)\1$/mg, '$2') - - // Expand newlines if double quoted - if (maybeQuote === '"') { - value = value.replace(/\\n/g, '\n') - value = value.replace(/\\r/g, '\r') - } - - // Add to object - obj[key] = value - } - - return obj -} - -function _log (message) { - console.log(`[dotenv][DEBUG] ${message}`) -} - -function _resolveHome (envPath) { - return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath -} - -// Populates process.env from .env file -function config (options) { - let dotenvPath = path.resolve(process.cwd(), '.env') - let encoding = 'utf8' - const debug = Boolean(options && options.debug) - const override = Boolean(options && options.override) - - if (options) { - if (options.path != null) { - dotenvPath = _resolveHome(options.path) - } - if (options.encoding != null) { - encoding = options.encoding - } - } - - try { - // Specifying an encoding returns a string instead of a buffer - const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding })) - - Object.keys(parsed).forEach(function (key) { - if (!Object.prototype.hasOwnProperty.call(process.env, key)) { - process.env[key] = parsed[key] - } else { - if (override === true) { - process.env[key] = parsed[key] - } - - if (debug) { - if (override === true) { - _log(`"${key}" is already defined in \`process.env\` and WAS overwritten`) - } else { - _log(`"${key}" is already defined in \`process.env\` and was NOT overwritten`) - } - } - } - }) - - return { parsed } - } catch (e) { - if (debug) { - _log(`Failed to load ${dotenvPath} ${e.message}`) - } - - return { error: e } - } -} - -const DotenvModule = { - config, - parse -} - -module.exports.config = DotenvModule.config -module.exports.parse = DotenvModule.parse -module.exports = DotenvModule diff --git a/tests/.env b/tests/.env index 30992572..8d227b35 100644 --- a/tests/.env +++ b/tests/.env @@ -16,14 +16,11 @@ DONT_EXPAND_SQUOTED='dontexpand\nnewlines' INLINE_COMMENTS=inline comments # work #very #well INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work -INLINE_COMMENTS_SPACE=inline comments must start with#space +INLINE_COMMENTS_SPACE=inline comments dont have to start with a#space EQUAL_SIGNS=equals== RETAIN_INNER_QUOTES={"foo": "bar"} -RETAIN_LEADING_DQUOTE="retained -RETAIN_LEADING_SQUOTE='retained -RETAIN_TRAILING_DQUOTE=retained" -RETAIN_TRAILING_SQUOTE=retained' RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}' TRIM_SPACE_FROM_UNQUOTED= some spaced out string USERNAME=therealnerdybeast@example.tld SPACED_KEY = parsed +UNQUOTED_ESCAPE_CHARACTERS=bar\\ bar diff --git a/tests/.env-multiline b/tests/.env-multiline index 2dffdcde..758b3377 100644 --- a/tests/.env-multiline +++ b/tests/.env-multiline @@ -30,7 +30,3 @@ IS A MULTILINE STRING' - -MULTI_UNENDED="THIS -LINE HAS -NO END QUOTE \ No newline at end of file diff --git a/tests/.env-multiline2 b/tests/.env-multiline.backup similarity index 94% rename from tests/.env-multiline2 rename to tests/.env-multiline.backup index 758b3377..2dffdcde 100644 --- a/tests/.env-multiline2 +++ b/tests/.env-multiline.backup @@ -30,3 +30,7 @@ IS A MULTILINE STRING' + +MULTI_UNENDED="THIS +LINE HAS +NO END QUOTE \ No newline at end of file diff --git a/tests/.env2 b/tests/.env.backup similarity index 83% rename from tests/.env2 rename to tests/.env.backup index 8d227b35..30992572 100644 --- a/tests/.env2 +++ b/tests/.env.backup @@ -16,11 +16,14 @@ DONT_EXPAND_SQUOTED='dontexpand\nnewlines' INLINE_COMMENTS=inline comments # work #very #well INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work -INLINE_COMMENTS_SPACE=inline comments dont have to start with a#space +INLINE_COMMENTS_SPACE=inline comments must start with#space EQUAL_SIGNS=equals== RETAIN_INNER_QUOTES={"foo": "bar"} +RETAIN_LEADING_DQUOTE="retained +RETAIN_LEADING_SQUOTE='retained +RETAIN_TRAILING_DQUOTE=retained" +RETAIN_TRAILING_SQUOTE=retained' RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}' TRIM_SPACE_FROM_UNQUOTED= some spaced out string USERNAME=therealnerdybeast@example.tld SPACED_KEY = parsed -UNQUOTED_ESCAPE_CHARACTERS=bar\\ bar diff --git a/tests/test-config.js b/tests/test-config.js index 8ce39a92..f87ca05e 100644 --- a/tests/test-config.js +++ b/tests/test-config.js @@ -11,8 +11,6 @@ const mockParseResponse = { test: 'foo' } let readFileSyncStub let parseStub -t.plan(14) - t.beforeEach(() => { readFileSyncStub = sinon.stub(fs, 'readFileSync').returns('test=foo') parseStub = sinon.stub(dotenv, 'parse').returns(mockParseResponse) @@ -63,13 +61,6 @@ t.test('takes option for debug', ct => { logStub.restore() }) -t.test('takes option for multiline', ct => { - ct.plan(1) - const testMultiline = true - dotenv.config({ multiline: testMultiline }) - ct.equal(parseStub.args[0][1].multiline, testMultiline) -}) - t.test('reads path with encoding, parsing output to process.env', ct => { ct.plan(2) diff --git a/tests/test-config2.js b/tests/test-config.js.backup similarity index 94% rename from tests/test-config2.js rename to tests/test-config.js.backup index d3731aba..8ce39a92 100644 --- a/tests/test-config2.js +++ b/tests/test-config.js.backup @@ -5,12 +5,14 @@ const path = require('path') const sinon = require('sinon') const t = require('tap') -const dotenv = require('../lib/main2') +const dotenv = require('../lib/main') const mockParseResponse = { test: 'foo' } let readFileSyncStub let parseStub +t.plan(14) + t.beforeEach(() => { readFileSyncStub = sinon.stub(fs, 'readFileSync').returns('test=foo') parseStub = sinon.stub(dotenv, 'parse').returns(mockParseResponse) @@ -61,6 +63,13 @@ t.test('takes option for debug', ct => { logStub.restore() }) +t.test('takes option for multiline', ct => { + ct.plan(1) + const testMultiline = true + dotenv.config({ multiline: testMultiline }) + ct.equal(parseStub.args[0][1].multiline, testMultiline) +}) + t.test('reads path with encoding, parsing output to process.env', ct => { ct.plan(2) diff --git a/tests/test-parse-multiline.js b/tests/test-parse-multiline.js index 37166da3..c2954598 100644 --- a/tests/test-parse-multiline.js +++ b/tests/test-parse-multiline.js @@ -1,13 +1,9 @@ const fs = require('fs') - -const sinon = require('sinon') const t = require('tap') const dotenv = require('../lib/main') -const parsed = dotenv.parse(fs.readFileSync('tests/.env-multiline', { encoding: 'utf8' }), { multiline: 'true' }) - -t.plan(26) +const parsed = dotenv.parse(fs.readFileSync('tests/.env-multiline', { encoding: 'utf8' })) t.type(parsed, Object, 'should return an object') @@ -49,8 +45,6 @@ t.equal(parsed.MULTI_DOUBLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING', 'parses mu t.equal(parsed.MULTI_SINGLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING', 'parses multi-line strings when using single quotes') -t.equal(parsed.MULTI_UNENDED, 'THIS\nLINE HAS\nNO END QUOTE', 'parses multi-line strings when using single quotes') - const payload = dotenv.parse(Buffer.from('BUFFER=true')) t.equal(payload.BUFFER, 'true', 'should parse a buffer into an object') @@ -64,9 +58,3 @@ t.same(NPayload, expectedPayload, 'can parse (\\n) line endings') const RNPayload = dotenv.parse(Buffer.from('SERVER=localhost\r\nPASSWORD=password\r\nDB=tests\r\n')) t.same(RNPayload, expectedPayload, 'can parse (\\r\\n) line endings') - -// test debug path -const logStub = sinon.stub(console, 'log') -dotenv.parse(Buffer.from('what is this'), { debug: true }) -t.ok(logStub.called) -logStub.restore() diff --git a/tests/test-parse2-multiline.js b/tests/test-parse-multiline.js.backup similarity index 86% rename from tests/test-parse2-multiline.js rename to tests/test-parse-multiline.js.backup index 30ff53c5..37166da3 100644 --- a/tests/test-parse2-multiline.js +++ b/tests/test-parse-multiline.js.backup @@ -1,9 +1,13 @@ const fs = require('fs') + +const sinon = require('sinon') const t = require('tap') -const dotenv = require('../lib/main2') +const dotenv = require('../lib/main') + +const parsed = dotenv.parse(fs.readFileSync('tests/.env-multiline', { encoding: 'utf8' }), { multiline: 'true' }) -const parsed = dotenv.parse(fs.readFileSync('tests/.env-multiline2', { encoding: 'utf8' })) +t.plan(26) t.type(parsed, Object, 'should return an object') @@ -45,6 +49,8 @@ t.equal(parsed.MULTI_DOUBLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING', 'parses mu t.equal(parsed.MULTI_SINGLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING', 'parses multi-line strings when using single quotes') +t.equal(parsed.MULTI_UNENDED, 'THIS\nLINE HAS\nNO END QUOTE', 'parses multi-line strings when using single quotes') + const payload = dotenv.parse(Buffer.from('BUFFER=true')) t.equal(payload.BUFFER, 'true', 'should parse a buffer into an object') @@ -58,3 +64,9 @@ t.same(NPayload, expectedPayload, 'can parse (\\n) line endings') const RNPayload = dotenv.parse(Buffer.from('SERVER=localhost\r\nPASSWORD=password\r\nDB=tests\r\n')) t.same(RNPayload, expectedPayload, 'can parse (\\r\\n) line endings') + +// test debug path +const logStub = sinon.stub(console, 'log') +dotenv.parse(Buffer.from('what is this'), { debug: true }) +t.ok(logStub.called) +logStub.restore() diff --git a/tests/test-parse.js b/tests/test-parse.js index f6859353..7f3f7c4c 100644 --- a/tests/test-parse.js +++ b/tests/test-parse.js @@ -1,14 +1,10 @@ const fs = require('fs') - -const sinon = require('sinon') const t = require('tap') const dotenv = require('../lib/main') const parsed = dotenv.parse(fs.readFileSync('tests/.env', { encoding: 'utf8' })) -t.plan(35) - t.type(parsed, Object, 'should return an object') t.equal(parsed.BASIC, 'basic', 'sets basic environment variable') @@ -43,19 +39,15 @@ t.equal(parsed.INLINE_COMMENTS_SINGLE_QUOTES, 'inline comments outside of #singl t.equal(parsed.INLINE_COMMENTS_DOUBLE_QUOTES, 'inline comments outside of #doublequotes', 'ignores inline comments, but respects # character inside of double quotes') -t.equal(parsed.INLINE_COMMENTS_SPACE, 'inline comments must start with#space', 'respects # character in values when it is not preceded by a space character') +t.equal(parsed.INLINE_COMMENTS_SPACE, 'inline comments dont have to start with a', 'respects # character in values when it is not preceded by a space character') t.equal(parsed.EQUAL_SIGNS, 'equals==', 'respects equals signs in values') t.equal(parsed.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes') -t.equal(parsed.RETAIN_LEADING_DQUOTE, '"retained', 'retains leading double quote') - -t.equal(parsed.RETAIN_LEADING_SQUOTE, "'retained", 'retains leading single quote') - -t.equal(parsed.RETAIN_TRAILING_DQUOTE, 'retained"', 'reatins trailing double quote') +t.equal(parsed.EQUAL_SIGNS, 'equals==', 'respects equals signs in values') -t.equal(parsed.RETAIN_TRAILING_SQUOTE, "retained'", 'retains trailing single quote') +t.equal(parsed.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes') t.equal(parsed.RETAIN_INNER_QUOTES_AS_STRING, '{"foo": "bar"}', 'retains inner quotes') @@ -78,21 +70,3 @@ t.same(NPayload, expectedPayload, 'can parse (\\n) line endings') const RNPayload = dotenv.parse(Buffer.from('SERVER=localhost\r\nPASSWORD=password\r\nDB=tests\r\n')) t.same(RNPayload, expectedPayload, 'can parse (\\r\\n) line endings') - -// test debug path -let logStub = sinon.stub(console, 'log') -dotenv.parse(Buffer.from('what is this'), { debug: true }) -t.ok(logStub.calledOnce) -logStub.restore() - -// test that debug in windows (\r\n lines) logs never -logStub = sinon.stub(console, 'log') -dotenv.parse(Buffer.from('HEY=there\r\n'), { debug: true }) -t.equal(logStub.called, false) -logStub.restore() - -// test that debug in windows (\r\n lines) logs when a repeated key -logStub = sinon.stub(console, 'log') -dotenv.parse(Buffer.from('what is this\r\n'), { debug: true }) -t.equal(logStub.called, true) -logStub.restore() diff --git a/tests/test-parse2.js b/tests/test-parse.js.backup similarity index 70% rename from tests/test-parse2.js rename to tests/test-parse.js.backup index 79ad64f1..f6859353 100644 --- a/tests/test-parse2.js +++ b/tests/test-parse.js.backup @@ -1,9 +1,13 @@ const fs = require('fs') + +const sinon = require('sinon') const t = require('tap') -const dotenv = require('../lib/main2') +const dotenv = require('../lib/main') + +const parsed = dotenv.parse(fs.readFileSync('tests/.env', { encoding: 'utf8' })) -const parsed = dotenv.parse(fs.readFileSync('tests/.env2', { encoding: 'utf8' })) +t.plan(35) t.type(parsed, Object, 'should return an object') @@ -39,15 +43,19 @@ t.equal(parsed.INLINE_COMMENTS_SINGLE_QUOTES, 'inline comments outside of #singl t.equal(parsed.INLINE_COMMENTS_DOUBLE_QUOTES, 'inline comments outside of #doublequotes', 'ignores inline comments, but respects # character inside of double quotes') -t.equal(parsed.INLINE_COMMENTS_SPACE, 'inline comments dont have to start with a', 'respects # character in values when it is not preceded by a space character') +t.equal(parsed.INLINE_COMMENTS_SPACE, 'inline comments must start with#space', 'respects # character in values when it is not preceded by a space character') t.equal(parsed.EQUAL_SIGNS, 'equals==', 'respects equals signs in values') t.equal(parsed.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes') -t.equal(parsed.EQUAL_SIGNS, 'equals==', 'respects equals signs in values') +t.equal(parsed.RETAIN_LEADING_DQUOTE, '"retained', 'retains leading double quote') -t.equal(parsed.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes') +t.equal(parsed.RETAIN_LEADING_SQUOTE, "'retained", 'retains leading single quote') + +t.equal(parsed.RETAIN_TRAILING_DQUOTE, 'retained"', 'reatins trailing double quote') + +t.equal(parsed.RETAIN_TRAILING_SQUOTE, "retained'", 'retains trailing single quote') t.equal(parsed.RETAIN_INNER_QUOTES_AS_STRING, '{"foo": "bar"}', 'retains inner quotes') @@ -70,3 +78,21 @@ t.same(NPayload, expectedPayload, 'can parse (\\n) line endings') const RNPayload = dotenv.parse(Buffer.from('SERVER=localhost\r\nPASSWORD=password\r\nDB=tests\r\n')) t.same(RNPayload, expectedPayload, 'can parse (\\r\\n) line endings') + +// test debug path +let logStub = sinon.stub(console, 'log') +dotenv.parse(Buffer.from('what is this'), { debug: true }) +t.ok(logStub.calledOnce) +logStub.restore() + +// test that debug in windows (\r\n lines) logs never +logStub = sinon.stub(console, 'log') +dotenv.parse(Buffer.from('HEY=there\r\n'), { debug: true }) +t.equal(logStub.called, false) +logStub.restore() + +// test that debug in windows (\r\n lines) logs when a repeated key +logStub = sinon.stub(console, 'log') +dotenv.parse(Buffer.from('what is this\r\n'), { debug: true }) +t.equal(logStub.called, true) +logStub.restore() diff --git a/tests/types/test.ts b/tests/types/test.ts index 6d14b57d..fcaf5f11 100644 --- a/tests/types/test.ts +++ b/tests/types/test.ts @@ -10,20 +10,10 @@ config({ debug: true, }); -config({ - multiline: true -}); - -config({ - multiline: false -}); - -parse("test", { multiline: true}); +parse("test"); const parsed = parse("NODE_ENV=production\nDB_HOST=a.b.c"); const dbHost: string = parsed["DB_HOST"]; -const parsedFromBuffer = parse(new Buffer("JUSTICE=league\n"), { - debug: true -}); +const parsedFromBuffer = parse(new Buffer("JUSTICE=league\n")); const justice: string = parsedFromBuffer["JUSTICE"]; From fd767763a0511611b2876f46d64c883a479d4bfe Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Tue, 25 Jan 2022 23:36:32 -0800 Subject: [PATCH 05/10] Remove backup files and bump to 15.0.0 --- lib/main.backup.js | 142 --------------------- package-lock.json | 4 +- package.json | 2 +- tests/.env-multiline.backup | 36 ------ tests/.env.backup | 29 ----- tests/test-config.js.backup | 180 --------------------------- tests/test-parse-multiline.js.backup | 72 ----------- tests/test-parse.js.backup | 98 --------------- 8 files changed, 3 insertions(+), 560 deletions(-) delete mode 100644 lib/main.backup.js delete mode 100644 tests/.env-multiline.backup delete mode 100644 tests/.env.backup delete mode 100644 tests/test-config.js.backup delete mode 100644 tests/test-parse-multiline.js.backup delete mode 100644 tests/test-parse.js.backup diff --git a/lib/main.backup.js b/lib/main.backup.js deleted file mode 100644 index 872b5a79..00000000 --- a/lib/main.backup.js +++ /dev/null @@ -1,142 +0,0 @@ -const fs = require('fs') -const path = require('path') -const os = require('os') - -function log (message) { - console.log(`[dotenv][DEBUG] ${message}`) -} - -const NEWLINE = '\n' -const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*("[^"]*"|'[^']*'|.*?)(\s+#.*)?$/ -const RE_NEWLINES = /\\n/g -const NEWLINES_MATCH = /\r\n|\n|\r/ - -// Parses src into an Object -function parse (src, options) { - const debug = Boolean(options && options.debug) - const multiline = Boolean(options && options.multiline) - const obj = {} - - // convert Buffers before splitting into lines and processing - const lines = src.toString().split(NEWLINES_MATCH) - - for (let idx = 0; idx < lines.length; idx++) { - let line = lines[idx] - - // matching "KEY' and 'VAL' in 'KEY=VAL' - const keyValueArr = line.match(RE_INI_KEY_VAL) - // matched? - if (keyValueArr != null) { - const key = keyValueArr[1] - // default undefined or missing values to empty string - let val = (keyValueArr[2] || '') - let end = val.length - 1 - const isDoubleQuoted = val[0] === '"' && val[end] === '"' - const isSingleQuoted = val[0] === "'" && val[end] === "'" - - const isMultilineDoubleQuoted = val[0] === '"' && val[end] !== '"' - const isMultilineSingleQuoted = val[0] === "'" && val[end] !== "'" - - // if parsing line breaks and the value starts with a quote - if (multiline && (isMultilineDoubleQuoted || isMultilineSingleQuoted)) { - const quoteChar = isMultilineDoubleQuoted ? '"' : "'" - - val = val.substring(1) - - while (idx++ < lines.length - 1) { - line = lines[idx] - end = line.length - 1 - if (line[end] === quoteChar) { - val += NEWLINE + line.substring(0, end) - break - } - val += NEWLINE + line - } - // if single or double quoted, remove quotes - } else if (isSingleQuoted || isDoubleQuoted) { - val = val.substring(1, end) - - // if double quoted, expand newlines - if (isDoubleQuoted) { - val = val.replace(RE_NEWLINES, NEWLINE) - } - } else { - // remove surrounding whitespace - val = val.trim() - } - - obj[key] = val - } else if (debug) { - const trimmedLine = line.trim() - - // ignore empty and commented lines - if (trimmedLine.length && trimmedLine[0] !== '#') { - log(`Failed to match key and value when parsing line ${idx + 1}: ${line}`) - } - } - } - - return obj -} - -function resolveHome (envPath) { - return envPath[0] === '~' ? path.join(os.homedir(), envPath.slice(1)) : envPath -} - -// Populates process.env from .env file -function config (options) { - let dotenvPath = path.resolve(process.cwd(), '.env') - let encoding = 'utf8' - const debug = Boolean(options && options.debug) - const override = Boolean(options && options.override) - const multiline = Boolean(options && options.multiline) - - if (options) { - if (options.path != null) { - dotenvPath = resolveHome(options.path) - } - if (options.encoding != null) { - encoding = options.encoding - } - } - - try { - // specifying an encoding returns a string instead of a buffer - const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding }), { debug, multiline }) - - Object.keys(parsed).forEach(function (key) { - if (!Object.prototype.hasOwnProperty.call(process.env, key)) { - process.env[key] = parsed[key] - } else { - if (override === true) { - process.env[key] = parsed[key] - } - - if (debug) { - if (override === true) { - log(`"${key}" is already defined in \`process.env\` and WAS overwritten`) - } else { - log(`"${key}" is already defined in \`process.env\` and was NOT overwritten`) - } - } - } - }) - - return { parsed } - } catch (e) { - if (debug) { - log(`Failed to load ${dotenvPath} ${e.message}`) - } - - return { error: e } - } -} - -const DotenvModule = { - config, - parse -} - -module.exports.config = DotenvModule.config -module.exports.parse = DotenvModule.parse -module.exports = DotenvModule diff --git a/package-lock.json b/package-lock.json index 90fe5257..f4d42673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { "name": "dotenv", - "version": "14.3.2", + "version": "15.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "14.3.2", + "version": "15.0.0", "license": "BSD-2-Clause", "devDependencies": { "@types/node": "^17.0.9", diff --git a/package.json b/package.json index af166e7d..c1c97e8d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dotenv", - "version": "14.3.2", + "version": "15.0.0", "description": "Loads environment variables from .env file", "main": "lib/main.js", "types": "lib/main.d.ts", diff --git a/tests/.env-multiline.backup b/tests/.env-multiline.backup deleted file mode 100644 index 2dffdcde..00000000 --- a/tests/.env-multiline.backup +++ /dev/null @@ -1,36 +0,0 @@ -BASIC=basic - -# previous line intentionally left blank -AFTER_LINE=after_line -EMPTY= -SINGLE_QUOTES='single_quotes' -SINGLE_QUOTES_SPACED=' single quotes ' -DOUBLE_QUOTES="double_quotes" -DOUBLE_QUOTES_SPACED=" double quotes " -EXPAND_NEWLINES="expand\nnew\nlines" -DONT_EXPAND_UNQUOTED=dontexpand\nnewlines -DONT_EXPAND_SQUOTED='dontexpand\nnewlines' -# COMMENTS=work -EQUAL_SIGNS=equals== -RETAIN_INNER_QUOTES={"foo": "bar"} - -RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}' -TRIM_SPACE_FROM_UNQUOTED= some spaced out string -USERNAME=therealnerdybeast@example.tld - SPACED_KEY = parsed - -MULTI_DOUBLE_QUOTED="THIS -IS -A -MULTILINE -STRING" - -MULTI_SINGLE_QUOTED='THIS -IS -A -MULTILINE -STRING' - -MULTI_UNENDED="THIS -LINE HAS -NO END QUOTE \ No newline at end of file diff --git a/tests/.env.backup b/tests/.env.backup deleted file mode 100644 index 30992572..00000000 --- a/tests/.env.backup +++ /dev/null @@ -1,29 +0,0 @@ -BASIC=basic - -# previous line intentionally left blank -AFTER_LINE=after_line -EMPTY= -SINGLE_QUOTES='single_quotes' -SINGLE_QUOTES_SPACED=' single quotes ' -DOUBLE_QUOTES="double_quotes" -DOUBLE_QUOTES_SPACED=" double quotes " -DOUBLE_QUOTES_INSIDE_SINGLE='double "quotes" work inside single quotes' -SINGLE_QUOTES_INSIDE_DOUBLE="single 'quotes' work inside double quotes" -EXPAND_NEWLINES="expand\nnew\nlines" -DONT_EXPAND_UNQUOTED=dontexpand\nnewlines -DONT_EXPAND_SQUOTED='dontexpand\nnewlines' -# COMMENTS=work -INLINE_COMMENTS=inline comments # work #very #well -INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work -INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work -INLINE_COMMENTS_SPACE=inline comments must start with#space -EQUAL_SIGNS=equals== -RETAIN_INNER_QUOTES={"foo": "bar"} -RETAIN_LEADING_DQUOTE="retained -RETAIN_LEADING_SQUOTE='retained -RETAIN_TRAILING_DQUOTE=retained" -RETAIN_TRAILING_SQUOTE=retained' -RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}' -TRIM_SPACE_FROM_UNQUOTED= some spaced out string -USERNAME=therealnerdybeast@example.tld - SPACED_KEY = parsed diff --git a/tests/test-config.js.backup b/tests/test-config.js.backup deleted file mode 100644 index 8ce39a92..00000000 --- a/tests/test-config.js.backup +++ /dev/null @@ -1,180 +0,0 @@ -const fs = require('fs') -const os = require('os') -const path = require('path') - -const sinon = require('sinon') -const t = require('tap') - -const dotenv = require('../lib/main') - -const mockParseResponse = { test: 'foo' } -let readFileSyncStub -let parseStub - -t.plan(14) - -t.beforeEach(() => { - readFileSyncStub = sinon.stub(fs, 'readFileSync').returns('test=foo') - parseStub = sinon.stub(dotenv, 'parse').returns(mockParseResponse) -}) - -t.afterEach(() => { - readFileSyncStub.restore() - parseStub.restore() -}) - -t.test('takes option for path', ct => { - ct.plan(1) - - const testPath = 'tests/.env' - dotenv.config({ path: testPath }) - - ct.equal(readFileSyncStub.args[0][0], testPath) -}) - -t.test('takes option for path along with home directory char ~', ct => { - ct.plan(2) - const mockedHomedir = '/Users/dummy' - const homedirStub = sinon.stub(os, 'homedir').returns(mockedHomedir) - const testPath = '~/.env' - dotenv.config({ path: testPath }) - - ct.equal(readFileSyncStub.args[0][0], path.join(mockedHomedir, '.env')) - ct.ok(homedirStub.called) - homedirStub.restore() -}) - -t.test('takes option for encoding', ct => { - ct.plan(1) - - const testEncoding = 'latin1' - dotenv.config({ encoding: testEncoding }) - - ct.equal(readFileSyncStub.args[0][1].encoding, testEncoding) -}) - -t.test('takes option for debug', ct => { - ct.plan(1) - - const logStub = sinon.stub(console, 'log') - dotenv.config({ debug: 'true' }) - - ct.ok(logStub.called) - logStub.restore() -}) - -t.test('takes option for multiline', ct => { - ct.plan(1) - const testMultiline = true - dotenv.config({ multiline: testMultiline }) - ct.equal(parseStub.args[0][1].multiline, testMultiline) -}) - -t.test('reads path with encoding, parsing output to process.env', ct => { - ct.plan(2) - - const res = dotenv.config() - - ct.same(res.parsed, mockParseResponse) - ct.equal(readFileSyncStub.callCount, 1) -}) - -t.test('does not write over keys already in process.env', ct => { - ct.plan(2) - - const existing = 'bar' - process.env.test = existing - // 'foo' returned as value in `beforeEach`. should keep this 'bar' - const env = dotenv.config() - - ct.equal(env.parsed && env.parsed.test, mockParseResponse.test) - ct.equal(process.env.test, existing) -}) - -t.test('does write over keys already in process.env if override turned on', ct => { - ct.plan(2) - - const existing = 'bar' - process.env.test = existing - // 'foo' returned as value in `beforeEach`. should keep this 'bar' - const env = dotenv.config({ override: true }) - - ct.equal(env.parsed && env.parsed.test, mockParseResponse.test) - ct.equal(process.env.test, 'foo') -}) - -t.test( - 'does not write over keys already in process.env if the key has a falsy value', - ct => { - ct.plan(2) - - const existing = '' - process.env.test = existing - // 'foo' returned as value in `beforeEach`. should keep this '' - const env = dotenv.config() - - ct.equal(env.parsed && env.parsed.test, mockParseResponse.test) - // NB: process.env.test becomes undefined on Windows - ct.notOk(process.env.test) - } -) - -t.test( - 'does write over keys already in process.env if the key has a falsy value but override is set to true', - ct => { - ct.plan(2) - - const existing = '' - process.env.test = existing - // 'foo' returned as value in `beforeEach`. should keep this '' - const env = dotenv.config({ override: true }) - - ct.equal(env.parsed && env.parsed.test, mockParseResponse.test) - // NB: process.env.test becomes undefined on Windows - ct.ok(process.env.test) - } -) - -t.test('returns parsed object', ct => { - ct.plan(2) - - const env = dotenv.config() - - ct.notOk(env.error) - ct.same(env.parsed, mockParseResponse) -}) - -t.test('returns any errors thrown from reading file or parsing', ct => { - ct.plan(1) - - readFileSyncStub.throws() - const env = dotenv.config() - - ct.type(env.error, Error) -}) - -t.test('logs any errors thrown from reading file or parsing when in debug mode', ct => { - ct.plan(2) - - const logStub = sinon.stub(console, 'log') - - readFileSyncStub.throws() - const env = dotenv.config({ debug: true }) - - ct.ok(logStub.called) - ct.type(env.error, Error) - - logStub.restore() -}) - -t.test('logs any errors parsing when in debug and override mode', ct => { - ct.plan(1) - - const logStub = sinon.stub(console, 'log') - - dotenv.config({ debug: true, override: true }) - - ct.ok(logStub.called) - - logStub.restore() -}) diff --git a/tests/test-parse-multiline.js.backup b/tests/test-parse-multiline.js.backup deleted file mode 100644 index 37166da3..00000000 --- a/tests/test-parse-multiline.js.backup +++ /dev/null @@ -1,72 +0,0 @@ -const fs = require('fs') - -const sinon = require('sinon') -const t = require('tap') - -const dotenv = require('../lib/main') - -const parsed = dotenv.parse(fs.readFileSync('tests/.env-multiline', { encoding: 'utf8' }), { multiline: 'true' }) - -t.plan(26) - -t.type(parsed, Object, 'should return an object') - -t.equal(parsed.BASIC, 'basic', 'sets basic environment variable') - -t.equal(parsed.AFTER_LINE, 'after_line', 'reads after a skipped line') - -t.equal(parsed.EMPTY, '', 'defaults empty values to empty string') - -t.equal(parsed.SINGLE_QUOTES, 'single_quotes', 'escapes single quoted values') - -t.equal(parsed.SINGLE_QUOTES_SPACED, ' single quotes ', 'respects surrounding spaces in single quotes') - -t.equal(parsed.DOUBLE_QUOTES, 'double_quotes', 'escapes double quoted values') - -t.equal(parsed.DOUBLE_QUOTES_SPACED, ' double quotes ', 'respects surrounding spaces in double quotes') - -t.equal(parsed.EXPAND_NEWLINES, 'expand\nnew\nlines', 'expands newlines but only if double quoted') - -t.equal(parsed.DONT_EXPAND_UNQUOTED, 'dontexpand\\nnewlines', 'expands newlines but only if double quoted') - -t.equal(parsed.DONT_EXPAND_SQUOTED, 'dontexpand\\nnewlines', 'expands newlines but only if double quoted') - -t.notOk(parsed.COMMENTS, 'ignores commented lines') - -t.equal(parsed.EQUAL_SIGNS, 'equals==', 'respects equals signs in values') - -t.equal(parsed.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes') - -t.equal(parsed.RETAIN_INNER_QUOTES_AS_STRING, '{"foo": "bar"}', 'retains inner quotes') - -t.equal(parsed.TRIM_SPACE_FROM_UNQUOTED, 'some spaced out string', 'retains spaces in string') - -t.equal(parsed.USERNAME, 'therealnerdybeast@example.tld', 'parses email addresses completely') - -t.equal(parsed.SPACED_KEY, 'parsed', 'parses keys and values surrounded by spaces') - -t.equal(parsed.MULTI_DOUBLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING', 'parses multi-line strings when using double quotes') - -t.equal(parsed.MULTI_SINGLE_QUOTED, 'THIS\nIS\nA\nMULTILINE\nSTRING', 'parses multi-line strings when using single quotes') - -t.equal(parsed.MULTI_UNENDED, 'THIS\nLINE HAS\nNO END QUOTE', 'parses multi-line strings when using single quotes') - -const payload = dotenv.parse(Buffer.from('BUFFER=true')) -t.equal(payload.BUFFER, 'true', 'should parse a buffer into an object') - -const expectedPayload = { SERVER: 'localhost', PASSWORD: 'password', DB: 'tests' } - -const RPayload = dotenv.parse(Buffer.from('SERVER=localhost\rPASSWORD=password\rDB=tests\r')) -t.same(RPayload, expectedPayload, 'can parse (\\r) line endings') - -const NPayload = dotenv.parse(Buffer.from('SERVER=localhost\nPASSWORD=password\nDB=tests\n')) -t.same(NPayload, expectedPayload, 'can parse (\\n) line endings') - -const RNPayload = dotenv.parse(Buffer.from('SERVER=localhost\r\nPASSWORD=password\r\nDB=tests\r\n')) -t.same(RNPayload, expectedPayload, 'can parse (\\r\\n) line endings') - -// test debug path -const logStub = sinon.stub(console, 'log') -dotenv.parse(Buffer.from('what is this'), { debug: true }) -t.ok(logStub.called) -logStub.restore() diff --git a/tests/test-parse.js.backup b/tests/test-parse.js.backup deleted file mode 100644 index f6859353..00000000 --- a/tests/test-parse.js.backup +++ /dev/null @@ -1,98 +0,0 @@ -const fs = require('fs') - -const sinon = require('sinon') -const t = require('tap') - -const dotenv = require('../lib/main') - -const parsed = dotenv.parse(fs.readFileSync('tests/.env', { encoding: 'utf8' })) - -t.plan(35) - -t.type(parsed, Object, 'should return an object') - -t.equal(parsed.BASIC, 'basic', 'sets basic environment variable') - -t.equal(parsed.AFTER_LINE, 'after_line', 'reads after a skipped line') - -t.equal(parsed.EMPTY, '', 'defaults empty values to empty string') - -t.equal(parsed.SINGLE_QUOTES, 'single_quotes', 'escapes single quoted values') - -t.equal(parsed.SINGLE_QUOTES_SPACED, ' single quotes ', 'respects surrounding spaces in single quotes') - -t.equal(parsed.DOUBLE_QUOTES, 'double_quotes', 'escapes double quoted values') - -t.equal(parsed.DOUBLE_QUOTES_SPACED, ' double quotes ', 'respects surrounding spaces in double quotes') - -t.equal(parsed.DOUBLE_QUOTES_INSIDE_SINGLE, 'double "quotes" work inside single quotes', 'respects double quotes inside single quotes') - -t.equal(parsed.SINGLE_QUOTES_INSIDE_DOUBLE, "single 'quotes' work inside double quotes", 'respects single quotes inside double quotes') - -t.equal(parsed.EXPAND_NEWLINES, 'expand\nnew\nlines', 'expands newlines but only if double quoted') - -t.equal(parsed.DONT_EXPAND_UNQUOTED, 'dontexpand\\nnewlines', 'expands newlines but only if double quoted') - -t.equal(parsed.DONT_EXPAND_SQUOTED, 'dontexpand\\nnewlines', 'expands newlines but only if double quoted') - -t.notOk(parsed.COMMENTS, 'ignores commented lines') - -t.equal(parsed.INLINE_COMMENTS, 'inline comments', 'ignores inline comments') - -t.equal(parsed.INLINE_COMMENTS_SINGLE_QUOTES, 'inline comments outside of #singlequotes', 'ignores inline comments, but respects # character inside of single quotes') - -t.equal(parsed.INLINE_COMMENTS_DOUBLE_QUOTES, 'inline comments outside of #doublequotes', 'ignores inline comments, but respects # character inside of double quotes') - -t.equal(parsed.INLINE_COMMENTS_SPACE, 'inline comments must start with#space', 'respects # character in values when it is not preceded by a space character') - -t.equal(parsed.EQUAL_SIGNS, 'equals==', 'respects equals signs in values') - -t.equal(parsed.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes') - -t.equal(parsed.RETAIN_LEADING_DQUOTE, '"retained', 'retains leading double quote') - -t.equal(parsed.RETAIN_LEADING_SQUOTE, "'retained", 'retains leading single quote') - -t.equal(parsed.RETAIN_TRAILING_DQUOTE, 'retained"', 'reatins trailing double quote') - -t.equal(parsed.RETAIN_TRAILING_SQUOTE, "retained'", 'retains trailing single quote') - -t.equal(parsed.RETAIN_INNER_QUOTES_AS_STRING, '{"foo": "bar"}', 'retains inner quotes') - -t.equal(parsed.TRIM_SPACE_FROM_UNQUOTED, 'some spaced out string', 'retains spaces in string') - -t.equal(parsed.USERNAME, 'therealnerdybeast@example.tld', 'parses email addresses completely') - -t.equal(parsed.SPACED_KEY, 'parsed', 'parses keys and values surrounded by spaces') - -const payload = dotenv.parse(Buffer.from('BUFFER=true')) -t.equal(payload.BUFFER, 'true', 'should parse a buffer into an object') - -const expectedPayload = { SERVER: 'localhost', PASSWORD: 'password', DB: 'tests' } - -const RPayload = dotenv.parse(Buffer.from('SERVER=localhost\rPASSWORD=password\rDB=tests\r')) -t.same(RPayload, expectedPayload, 'can parse (\\r) line endings') - -const NPayload = dotenv.parse(Buffer.from('SERVER=localhost\nPASSWORD=password\nDB=tests\n')) -t.same(NPayload, expectedPayload, 'can parse (\\n) line endings') - -const RNPayload = dotenv.parse(Buffer.from('SERVER=localhost\r\nPASSWORD=password\r\nDB=tests\r\n')) -t.same(RNPayload, expectedPayload, 'can parse (\\r\\n) line endings') - -// test debug path -let logStub = sinon.stub(console, 'log') -dotenv.parse(Buffer.from('what is this'), { debug: true }) -t.ok(logStub.calledOnce) -logStub.restore() - -// test that debug in windows (\r\n lines) logs never -logStub = sinon.stub(console, 'log') -dotenv.parse(Buffer.from('HEY=there\r\n'), { debug: true }) -t.equal(logStub.called, false) -logStub.restore() - -// test that debug in windows (\r\n lines) logs when a repeated key -logStub = sinon.stub(console, 'log') -dotenv.parse(Buffer.from('what is this\r\n'), { debug: true }) -t.equal(logStub.called, true) -logStub.restore() From c906decccf623581a67f2ac1d2046176e4e48470 Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Wed, 26 Jan 2022 10:39:16 -0800 Subject: [PATCH 06/10] Clean up --- lib/main.js | 1 - tests/.env | 1 - 2 files changed, 2 deletions(-) diff --git a/lib/main.js b/lib/main.js index eef02afd..27f4620f 100644 --- a/lib/main.js +++ b/lib/main.js @@ -28,7 +28,6 @@ function parse (src) { const maybeQuote = value[0] // Remove surrounding quotes - // value = value.replace(/^(['"])(.*)\1$/mg, '$2') value = value.replace(/^(['"])([\s\S]+)\1$/mg, '$2') // Expand newlines if double quoted diff --git a/tests/.env b/tests/.env index 8d227b35..1ffb4ece 100644 --- a/tests/.env +++ b/tests/.env @@ -23,4 +23,3 @@ RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}' TRIM_SPACE_FROM_UNQUOTED= some spaced out string USERNAME=therealnerdybeast@example.tld SPACED_KEY = parsed -UNQUOTED_ESCAPE_CHARACTERS=bar\\ bar From 8efe3ede22eea6f5a97fb9d303d85e563676d70f Mon Sep 17 00:00:00 2001 From: Scott Motte Date: Wed, 26 Jan 2022 11:08:43 -0800 Subject: [PATCH 07/10] Update README --- README.md | 130 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 138df1bd..6b55bd36 100644 --- a/README.md +++ b/README.md @@ -28,54 +28,110 @@ Or installing with yarn? `yarn add dotenv` ## Usage -Usage is easy! - -### 1. Create a `.env` file in the **root directory** of your project. +Create a `.env` file in the root of your project: ```dosini -# .env file -# -# Add environment-specific variables on new lines in the form of NAME=VALUE -# -DB_HOST=localhost -DB_USER=root -DB_PASS=s1mpl3 +S3_BUCKET=YOURS3BUCKET +SECRET_KEY=YOURSECRETKEYGOESHERE ``` -### 2. As early as possible in your application, import and configure dotenv. +As early as possible in your application, import and configure dotenv: ```javascript -// index.js require('dotenv').config() - console.log(process.env) // remove this after you've confirmed it working ``` .. or using ES6? ```javascript -// index.mjs (ESM) import 'dotenv/config' // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import import express from 'express' ``` -### 3. That's it! 🎉 - -`process.env` now has the keys and values you defined in your `.env` file. +That's it. `process.env` now has the keys and values you defined in your `.env` file: ```javascript require('dotenv').config() ... -const db = require('db') -db.connect({ - host: process.env.DB_HOST, - username: process.env.DB_USER, - password: process.env.DB_PASS -}) +s3.getBucketCors({Bucket: process.env.S3_BUCKET}, function(err, data) {}) +``` + +### Multiline values + +If you need multiline variables, for example private keys, those are now supported (`>= v15.0.0`) with line breaks: + +```dosini +PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY----- +... +Kh9NV... +... +-----END DSA PRIVATE KEY-----" +``` + +Alternatively, you can double quote strings and use the `\n` character: + +```dosini +PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\Kh9NV...\n-----END DSA PRIVATE KEY-----\n" +``` + +### Comments + +Comments may be added to your file on their own line or inline: + +```dosini +# This is a comment +SECRET_KEY=YOURSECRETKEYGOESHERE # comment +SECRET_HASH="something-with-a-#-hash" +``` + +Comments begin where a `#` exists, so if your value contains a `#` please wrap it in quotes. This is a breaking change from `>= v15.0.0` and on. + +### Parsing + +The engine which parses the contents of your file containing environment variables is available to use. It accepts a String or Buffer and will return an Object with the parsed keys and values. + +```javascript +const dotenv = require('dotenv') +const buf = Buffer.from('BASIC=basic') +const config = dotenv.parse(buf) // will return an object +console.log(typeof config, config) // object { BASIC : 'basic' } +``` + +### Preload + +You can use the `--require` (`-r`) [command line option](https://nodejs.org/api/cli.html#cli_r_require_module) to preload dotenv. By doing this, you do not need to require and load dotenv in your application code. + +```bash +$ node -r dotenv/config your_script.js +``` + +The configuration options below are supported as command line arguments in the format `dotenv_config_