Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: basic escaping support #713

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 29 additions & 16 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,28 +22,41 @@ function parse (src) {
const key = match[1]

// Default undefined or null to empty string
let value = (match[2] || '')
const unProcessedValue = (match[2] || '')
const value = parseValue(unProcessedValue)
// Add to object
obj[key] = value
}

// Remove whitespace
value = value.trim()
return obj
}

// Check if double quoted
const maybeQuote = value[0]
function parseValue (value) {
// Remove whitespace
const trimmedValue = value.trim()
// Check if double quoted
const maybeQuote = trimmedValue[0]
// Remove surrounding quotes
const valueNoQuotes = trimmedValue.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')

// Remove surrounding quotes
value = value.replace(/^(['"`])([\s\S]*)\1$/mg, '$2')
return unescapeValue(valueNoQuotes, maybeQuote)
}

// Expand newlines if double quoted
if (maybeQuote === '"') {
value = value.replace(/\\n/g, '\n')
value = value.replace(/\\r/g, '\r')
}
function unescapeCharacters (value) {
return value.replace(/\\([^$])/g, '$1')
}

// Add to object
obj[key] = value
}
function expandNewLines (value) {
return value.replace(/\\n/g, '\n').replace(/\\r/g, '\r')
}

return obj
function unescapeValue (value, maybeQuote) {
// Expand newlines if double quoted
if (maybeQuote === '"') {
return unescapeCharacters(expandNewLines(value))
} else {
return unescapeCharacters(value)
}
}

function _log (message) {
Expand Down
7 changes: 7 additions & 0 deletions tests/.env-escaping
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
ESCAPE_CHAR=ab\cde
ESCAPE_NEWLINE_NO_QUOTE=line1\nline2
ESCAPE_NEWLINE_IN_DOUBLE_QUOTE="line1\nline2"
ESCAPE_QUOTES=single quote: \', double quote: \", backtick: \`
ESCAPE_TAB=\t
ESCAPE_QUOTES_IN_QUOTES="John said: \"Lets go\""
ESCAPE_DOLLAR=\$\$\$
22 changes: 22 additions & 0 deletions tests/test-parse-escaping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const fs = require('fs')
const t = require('tap')

const dotenv = require('../lib/main')

const parsed = dotenv.parse(fs.readFileSync('tests/.env-escaping', { encoding: 'utf8' }))

t.type(parsed, Object, 'should return an object')

t.equal(parsed.ESCAPE_CHAR, 'abcde', 'a "regular" escaped character should not be modified by un-escaping')

t.equal(parsed.ESCAPE_QUOTES, 'single quote: \', double quote: ", backtick: `', 'quote characters can be escaped just like any other "regular" character')

t.equal(parsed.ESCAPE_NEWLINE_NO_QUOTE, 'line1nline2', 'without double quotes `\\n` is un-escaped to `n`')

t.equal(parsed.ESCAPE_NEWLINE_IN_DOUBLE_QUOTE, 'line1\nline2', 'with double quotes `\\n` is expanded to `\n`')

t.equal(parsed.ESCAPE_TAB, 't', '\\t has no special meaning (expansion) in this library')

t.equal(parsed.ESCAPE_QUOTES_IN_QUOTES, 'John said: "Lets go"', 'quotes can be escaped inside quoted dotenv value')

t.equal(parsed.ESCAPE_DOLLAR, '\\$\\$\\$', 'dollar sign is never escaped')
4 changes: 2 additions & 2 deletions tests/test-parse-multiline.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ t.equal(parsed.DOUBLE_QUOTES_SPACED, ' double quotes ', 'respects surround

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_UNQUOTED, 'dontexpandnnewlines', 'expands newlines but only if double quoted')
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i have mixed feelings on this behavior. looks like the rubygem has done this since inception, but it doesn't follow the principle of least surprise to me. the \n disappearing would frustrate me.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this behavior goes back to 0.0.3. 599144a

i'm open to arguments for doing this, but it will definitely be a potentially significant breaking change if we do


t.equal(parsed.DONT_EXPAND_SQUOTED, 'dontexpand\\nnewlines', 'expands newlines but only if double quoted')
t.equal(parsed.DONT_EXPAND_SQUOTED, 'dontexpandnnewlines', 'expands newlines but only if double quoted')

t.notOk(parsed.COMMENTS, 'ignores commented lines')

Expand Down
4 changes: 2 additions & 2 deletions tests/test-parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@ t.equal(parsed.DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS, "double \"quotes\" and

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_UNQUOTED, 'dontexpandnnewlines', 'expands newlines but only if double quoted')

t.equal(parsed.DONT_EXPAND_SQUOTED, 'dontexpand\\nnewlines', 'expands newlines but only if double quoted')
t.equal(parsed.DONT_EXPAND_SQUOTED, 'dontexpandnnewlines', 'expands newlines but only if double quoted')

t.notOk(parsed.COMMENTS, 'ignores commented lines')

Expand Down