Skip to content

Commit c37478d

Browse files
committed
Merge branch 'andreialecu-feat-multiline'
2 parents 44281f4 + 8875300 commit c37478d

12 files changed

+270
-13
lines changed

README.md

+55
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ Turn on logging to help debug why certain keys or values are not being set as yo
151151
require('dotenv').config({ debug: process.env.DEBUG })
152152
```
153153

154+
<<<<<<< HEAD
154155
##### Override
155156

156157
Default: `false`
@@ -162,6 +163,32 @@ require('dotenv').config({ override: true })
162163
```
163164

164165
### Parse
166+
=======
167+
#### Multiline
168+
169+
Default: `default`
170+
171+
You may specify the value `line-breaks` to switch the parser into a mode in which line breaks
172+
inside quoted values are allowed.
173+
174+
```js
175+
require('dotenv').config({ multiline: 'line-breaks' })
176+
```
177+
178+
This allows specifying multiline values in this format:
179+
180+
```
181+
PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
182+
MIGT...
183+
7ure...
184+
-----END PRIVATE KEY-----"
185+
```
186+
187+
Ensure that the value begins with a single or double quote character, and it ends with the same character.
188+
189+
190+
## Parse
191+
>>>>>>> 9b1d338e76daa73fa4fb8ed27b94082d80310eba
165192
166193
The engine which parses the contents of your file containing environment
167194
variables is available to use. It accepts a String or Buffer and will return
@@ -194,6 +221,7 @@ const config = dotenv.parse(buf, opt)
194221

195222
### Preload
196223

224+
<<<<<<< HEAD
197225
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. This is the preferred approach when using `import` instead of `require`.
198226

199227
```bash
@@ -215,6 +243,33 @@ $ DOTENV_CONFIG_<OPTION>=value node -r dotenv/config your_script.js
215243
```bash
216244
$ DOTENV_CONFIG_ENCODING=latin1 DOTENV_CONFIG_DEBUG=true node -r dotenv/config your_script.js dotenv_config_path=/custom/path/to/.env
217245
```
246+
=======
247+
- `BASIC=basic` becomes `{BASIC: 'basic'}`
248+
- empty lines are skipped
249+
- lines beginning with `#` are treated as comments
250+
- empty values become empty strings (`EMPTY=` becomes `{EMPTY: ''}`)
251+
- inner quotes are maintained (think JSON) (`JSON={"foo": "bar"}` becomes `{JSON:"{\"foo\": \"bar\"}"`)
252+
- whitespace is removed from both ends of unquoted values (see more on [`trim`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim)) (`FOO= some value ` becomes `{FOO: 'some value'}`)
253+
- single and double quoted values are escaped (`SINGLE_QUOTE='quoted'` becomes `{SINGLE_QUOTE: "quoted"}`)
254+
- single and double quoted values maintain whitespace from both ends (`FOO=" some value "` becomes `{FOO: ' some value '}`)
255+
- double quoted values expand new lines. Example: `MULTILINE="new\nline"` becomes
256+
257+
```
258+
{MULTILINE: 'new
259+
line'}
260+
```
261+
- multi-line values with line breaks are supported for quoted values if using the `{ multiline: "line-break" }` option.
262+
In this mode you do not need to use `\n` to separate lines. Example:
263+
264+
```
265+
PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
266+
MIGT...
267+
7ure...
268+
-----END PRIVATE KEY-----"
269+
```
270+
271+
Note that when using this option, all values that start with quotes must end in quotes.
272+
>>>>>>> 9b1d338e76daa73fa4fb8ed27b94082d80310eba
218273
219274
## FAQ
220275

lib/cli-options.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const re = /^dotenv_config_(encoding|path|debug|override)=(.+)$/
1+
const re = /^dotenv_config_(encoding|path|debug|override|multiline)=(.+)$/
22

33
module.exports = function optionMatcher (args) {
44
return args.reduce(function (acc, cur) {

lib/env-options.js

+4
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,8 @@ if (process.env.DOTENV_CONFIG_OVERRIDE != null) {
1717
options.override = process.env.DOTENV_CONFIG_OVERRIDE
1818
}
1919

20+
if (process.env.DOTENV_CONFIG_MULTILINE != null) {
21+
options.multiline = process.env.DOTENV_CONFIG_MULTILINE
22+
}
23+
2024
module.exports = options

lib/main.d.ts

+31-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ export interface DotenvParseOptions {
1010
* example: `dotenv.parse('KEY=value', { debug: true })`
1111
*/
1212
debug?: boolean;
13+
14+
/**
15+
* Default: `false`
16+
*
17+
* Turn on multiline line break parsing.
18+
*
19+
* example:
20+
*
21+
* MY_VAR="this
22+
* is
23+
* a
24+
* multiline
25+
* string"
26+
*/
27+
multiline?: boolean;
1328
}
1429

1530
export interface DotenvParseOutput {
@@ -66,6 +81,21 @@ export interface DotenvConfigOptions {
6681
* example: `require('dotenv').config({ override: true })`
6782
*/
6883
override?: boolean;
84+
85+
/**
86+
* Default: `false`
87+
*
88+
* Turn on multiline line break parsing.
89+
*
90+
* example:
91+
*
92+
* MY_VAR="this
93+
* is
94+
* a
95+
* multiline
96+
* string"
97+
*/
98+
multiline?: boolean;
6999
}
70100

71101
export interface DotenvConfigOutput {
@@ -78,7 +108,7 @@ export interface DotenvConfigOutput {
78108
*
79109
* See https://docs.dotenv.org
80110
*
81-
* @param options - additional options. example: `{ path: './custom/path', encoding: 'latin1', debug: true, override: false }`
111+
* @param options - additional options. example: `{ path: './custom/path', encoding: 'latin1', debug: true, override: false, multiline: false }`
82112
* @returns an object with a `parsed` key if successful or `error` key if an error occurred. example: { parsed: { KEY: 'value' } }
83113
*
84114
*/

lib/main.js

+35-7
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,46 @@ const NEWLINES_MATCH = /\r\n|\n|\r/
1414
// Parses src into an Object
1515
function parse (src, options) {
1616
const debug = Boolean(options && options.debug)
17+
const multiline = Boolean(options && options.multiline)
1718
const obj = {}
1819

1920
// convert Buffers before splitting into lines and processing
20-
src.toString().split(NEWLINES_MATCH).forEach(function (line, idx) {
21+
const lines = src.toString().split(NEWLINES_MATCH)
22+
23+
for (let idx = 0; idx < lines.length; idx++) {
24+
let line = lines[idx]
25+
2126
// matching "KEY' and 'VAL' in 'KEY=VAL'
2227
const keyValueArr = line.match(RE_INI_KEY_VAL)
2328
// matched?
2429
if (keyValueArr != null) {
2530
const key = keyValueArr[1]
2631
// default undefined or missing values to empty string
2732
let val = (keyValueArr[2] || '')
28-
const end = val.length - 1
33+
let end = val.length - 1
2934
const isDoubleQuoted = val[0] === '"' && val[end] === '"'
3035
const isSingleQuoted = val[0] === "'" && val[end] === "'"
3136

37+
const isMultilineDoubleQuoted = val[0] === '"' && val[end] !== '"'
38+
const isMultilineSingleQuoted = val[0] === "'" && val[end] !== "'"
39+
40+
// if parsing line breaks and the value starts with a quote
41+
if (multiline && (isMultilineDoubleQuoted || isMultilineSingleQuoted)) {
42+
const quoteChar = isMultilineDoubleQuoted ? '"' : "'"
43+
44+
val = val.substring(1)
45+
46+
while (idx++ < lines.length - 1) {
47+
line = lines[idx]
48+
end = line.length - 1
49+
if (line[end] === quoteChar) {
50+
val += NEWLINE + line.substring(0, end)
51+
break
52+
}
53+
val += NEWLINE + line
54+
}
3255
// if single or double quoted, remove quotes
33-
if (isSingleQuoted || isDoubleQuoted) {
56+
} else if (isSingleQuoted || isDoubleQuoted) {
3457
val = val.substring(1, end)
3558

3659
// if double quoted, expand newlines
@@ -51,7 +74,7 @@ function parse (src, options) {
5174
log(`Failed to match key and value when parsing line ${idx + 1}: ${line}`)
5275
}
5376
}
54-
})
77+
}
5578

5679
return obj
5780
}
@@ -66,6 +89,7 @@ function config (options) {
6689
let encoding = 'utf8'
6790
const debug = Boolean(options && options.debug)
6891
const override = Boolean(options && options.override)
92+
const multiline = Boolean(options && options.multiline)
6993

7094
if (options) {
7195
if (options.path != null) {
@@ -78,7 +102,7 @@ function config (options) {
78102

79103
try {
80104
// specifying an encoding returns a string instead of a buffer
81-
const parsed = parse(fs.readFileSync(dotenvPath, { encoding }), { debug })
105+
const parsed = DotenvModule.parse(fs.readFileSync(dotenvPath, { encoding }), { debug, multiline })
82106

83107
Object.keys(parsed).forEach(function (key) {
84108
if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
@@ -108,5 +132,9 @@ function config (options) {
108132
}
109133
}
110134

111-
module.exports.config = config
112-
module.exports.parse = parse
135+
const DotenvModule = {
136+
config,
137+
parse
138+
}
139+
140+
module.exports = DotenvModule

tests/.env-multiline

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
BASIC=basic
2+
3+
# previous line intentionally left blank
4+
AFTER_LINE=after_line
5+
EMPTY=
6+
SINGLE_QUOTES='single_quotes'
7+
SINGLE_QUOTES_SPACED=' single quotes '
8+
DOUBLE_QUOTES="double_quotes"
9+
DOUBLE_QUOTES_SPACED=" double quotes "
10+
EXPAND_NEWLINES="expand\nnew\nlines"
11+
DONT_EXPAND_UNQUOTED=dontexpand\nnewlines
12+
DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
13+
# COMMENTS=work
14+
EQUAL_SIGNS=equals==
15+
RETAIN_INNER_QUOTES={"foo": "bar"}
16+
17+
RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}'
18+
TRIM_SPACE_FROM_UNQUOTED= some spaced out string
19+
USERNAME=therealnerdybeast@example.tld
20+
SPACED_KEY = parsed
21+
22+
MULTI_DOUBLE_QUOTED="THIS
23+
IS
24+
A
25+
MULTILINE
26+
STRING"
27+
28+
MULTI_SINGLE_QUOTED='THIS
29+
IS
30+
A
31+
MULTILINE
32+
STRING'
33+
34+
MULTI_UNENDED="THIS
35+
LINE HAS
36+
NO END QUOTE

tests/test-cli-options.js

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ t.same(options(['node', '-e', "'console.log(testing)'", 'dotenv_config_override=
2424
override: 'true'
2525
})
2626

27+
// matches multiline option
28+
t.same(options(['node', '-e', "'console.log(testing)'", 'dotenv_config_multiline=true']), {
29+
multiline: 'true'
30+
})
31+
2732
// ignores empty values
2833
t.same(options(['node', '-e', "'console.log(testing)'", 'dotenv_config_path=']), {})
2934

tests/test-config-cli.js

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ const path = require('path')
33

44
const t = require('tap')
55

6+
const configPath = path.resolve(__dirname, '../config.js')
7+
68
function spawn (cmd, options = {}) {
79
const { stdout } = cp.spawnSync(
810
process.argv[0], // node binary
@@ -28,7 +30,7 @@ t.equal(
2830
spawn(
2931
[
3032
'-r',
31-
'./config',
33+
configPath,
3234
'-e',
3335
'console.log(process.env.BASIC)',
3436
'dotenv_config_encoding=utf8',
@@ -43,7 +45,7 @@ t.equal(
4345
spawn(
4446
[
4547
'-r',
46-
'./config',
48+
configPath,
4749
'-e',
4850
'console.log(process.env.BASIC)'
4951
],
@@ -61,7 +63,7 @@ t.equal(
6163
spawn(
6264
[
6365
'-r',
64-
'./config',
66+
configPath,
6567
'-e',
6668
'console.log(process.env.BASIC)',
6769
'dotenv_config_path=./tests/.env'

tests/test-config.js

+7
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ t.test('takes option for debug', ct => {
6363
logStub.restore()
6464
})
6565

66+
t.test('takes option for multiline', ct => {
67+
ct.plan(1)
68+
const testMultiline = true
69+
dotenv.config({ multiline: testMultiline })
70+
ct.equal(parseStub.args[0][1].multiline, testMultiline)
71+
})
72+
6673
t.test('reads path with encoding, parsing output to process.env', ct => {
6774
ct.plan(2)
6875

tests/test-env-options.js

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const e = process.env.DOTENV_CONFIG_ENCODING
99
const p = process.env.DOTENV_CONFIG_PATH
1010
const d = process.env.DOTENV_CONFIG_DEBUG
1111
const o = process.env.DOTENV_CONFIG_OVERRIDE
12+
const m = process.env.DOTENV_CONFIG_MULTILINE
1213

1314
// get fresh object for each test
1415
function options () {
@@ -32,6 +33,7 @@ delete process.env.DOTENV_CONFIG_ENCODING
3233
delete process.env.DOTENV_CONFIG_PATH
3334
delete process.env.DOTENV_CONFIG_DEBUG
3435
delete process.env.DOTENV_CONFIG_OVERRIDE
36+
delete process.env.DOTENV_CONFIG_MULTILINE
3537

3638
t.same(options(), {})
3739

@@ -47,8 +49,12 @@ testOption('DOTENV_CONFIG_DEBUG', 'true', { debug: 'true' })
4749
// sets override option
4850
testOption('DOTENV_CONFIG_OVERRIDE', 'true', { override: 'true' })
4951

52+
// sets multiline option
53+
testOption('DOTENV_CONFIG_MULTILINE', 'true', { multiline: 'true' })
54+
5055
// restore existing env
5156
process.env.DOTENV_CONFIG_ENCODING = e
5257
process.env.DOTENV_CONFIG_PATH = p
5358
process.env.DOTENV_CONFIG_DEBUG = d
5459
process.env.DOTENV_CONFIG_OVERRIDE = o
60+
process.env.DOTENV_CONFIG_MULTILINE = m

0 commit comments

Comments
 (0)