Dotenv is mainly used in Nodejs environments to add environment variables defined in the. Env file to the process.env object, which is to add environment variables to the process.env object by configuration. It is worth noting that this method is based on the principles of Storing configuration in the environment in Software Design 12.
The source code implementation of Dotenv is very simple, as shown below. Therefore, it is very suitable for those who have just begun to learn the source code. It helps to enhance the confidence of the source code. Otherwise, at the very beginning you will be discouraged if you do it!!
This article will cover the source code implementation in detail based on the 14.3.0 version of Dotenv, which is probably the most detailed source code parsing ever. Words do not say, leng hammer directly on dry goods!!
instructions
Env file is created in the project root directory, and variables can be configured in this file:
Define environment variables
DB_HOST=localhost
DB_USER=root
DB_PASS=s1mpl3
Copy the code
Call using the Config method exposed only by importing the library. Here’s an example from the official website:
require('dotenv').config();
/** * The following output is displayed: * {* DB_HOST: localhost, * DB_USER: root, * DB_PASS: s1mpl3 *} ** /
console.log(process.env);
Copy the code
You can also call parse directly for data that matches this configuration rule:
const dotenv = require('dotenv')
const buf = Buffer.from('BASIC=basic')
const config = dotenv.parse(buf)
// Output: object {BASIC: 'BASIC'}
console.log(typeof config, config)
Copy the code
Source code analysis
Dotenv’s source code is around 200+ lines, all in his lib/main.js. The overall structure is to export two functions, the code is as follows:
const fs = require('fs')
const path = require('path')
const os = require('os')
// Parses src into an Object
function parse (src, options) {}
// Populates process.env from .env file
function config (options) {}const DotenvModule = {
config,
parse
}
module.exports = DotenvModule
Copy the code
If you look closely, the Dotenv source code actually exposes only two methods:
const DotenvModule = {
config,
parse
}
module.exports = DotenvModule
Copy the code
The implementation of the config method
The config function reads the. Env variable configuration and adds it to the process.env object.
// Read the parse target configuration file and assign the parsed environment variables to the process.env object
function config (options) {
// By default, utf8 is used to parse. Env files
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) {
// Files with custom environment variables are preferred
if(options.path ! =null) {
dotenvPath = resolveHome(options.path)
}
// If the user specified the encoding format, it is preferred
if(options.encoding ! =null) {
encoding = options.encoding
}
}
try {
/** * - call fs.readFileSync * - call the enclosed parse function */
const parsed = DotenvModule.parse(
fs.readFileSync(dotenvPath, { encoding }),
{ debug, multiline }
)
/** * Assign key/value values obtained from parsing. Env to process.env */
Object.keys(parsed).forEach(function (key) {
if (!Object.prototype.hasOwnProperty.call(process.env, key)) {
process.env[key] = parsed[key]
} else {
Process. env determines whether to override existing keys based on the user's override configuration
if (override === true) {
process.env[key] = parsed[key]
}
// If debug mode is specified, duplicate keys will be prompted for log
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`)}}}})// After assigning the process.env object, the. Env file data is parsed and returned
return { parsed }
} catch (e) {
if (debug) {
log(`Failed to load ${dotenvPath} ${e.message}`)}return { error: e }
}
}
Copy the code
- Use the default
utf8
Encoding format passesfs.readFileSync
readThe root directoryUnder the.env
file - If the user uses a custom path, the configuration file from the defined path is read
- call
parse
Method will be.env
Data parsed intokey/value
In the form of - Directly to
process.env
For the assignment - If there are duplicates in assignment
key
According to the user configuration, you can choose to overwrite or output logs
Another point to note here is whether the resolveHome function gets the logic for the root path:
Env file path * If the path begins with ~, os.homedir() is called to obtain the corresponding system home path */
function resolveHome (envPath) {
return envPath[0= = ='~' ? path.join(os.homedir(), envPath.slice(1)) : envPath
}
Copy the code
The internal implementation of config, which retrieves all key/value sets, is obtained through the parse function. Let’s look at the implementation logic of Parse.
Parse method implementation
const NEWLINE = '\n'
/** * the regular ^\s*([\w.-]+)\s*= * - beginning is 0 - n space * - follow behind in both Chinese and English, underline, points, a hyphen tailgating 0 - n * - behind space * - is then followed by an equal sign * in the middle of the regular \ s * (" [^ "] * '|' [^ '] * '| [^ #] *)? * - followed by 0-n Spaces * - followed by (note 1) : * - The closing is a double quotation mark, followed by any other 0-N characters that are not double quotation marks * - or the closing is a single quotation mark, Middle is single quotes other # 0 - n characters * - or in addition to the extra 0 - n characters * - the final question mark said Overall are optional * note 1 last regular (| \ \ s * s * #. *)? $describes: * - followed by 0-n Spaces or 0-n Spaces plus # signs plus 0-n arbitrary characters */
const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\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
/** * Split each line of data with a line break * - Windows line break r n * - Mac line break r * - Unix line break n */
const lines = src.toString().split(NEWLINES_MATCH)
// Iterate over each row of data to get key and value as variables and variable names
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) {
// The subexpression 1 matches the key before the = sign
const key = keyValueArr[1]
// default undefined or missing values to empty string
// Subexpression 2 matches the value after the = sign, excluding the comment at the end of the line
let val = (keyValueArr[2] | |' ')
// The subscript of the last character of value
let end = val.length - 1
// Check whether value starts and ends with double quotation marks
const isDoubleQuoted = val[0= = ='"' && val[end] === '"'
// Check whether value starts and ends with single quotation marks
const isSingleQuoted = val[0= = ="'" && val[end] === "'"
// Check that value starts with a double quotation mark and ends without a double quotation mark
const isMultilineDoubleQuoted = val[0= = ='"'&& val[end] ! = ='"'
// Check that the value begins with a single quotation mark and ends without a double quotation mark
const isMultilineSingleQuoted = val[0= = ="'"&& val[end] ! = ="'"
// if parsing line breaks and the value starts with a quote
/** * If the following conditions are met: * - The user agrees to configure multiple lines *. The user continues to recursively query the next line until the end of a line matches the first single and double quotation marks */
if (multiline && (isMultilineDoubleQuoted || isMultilineSingleQuoted)) {
const quoteChar = isMultilineDoubleQuoted ? '"' : "'"
val = val.substring(1)
/** * Recursively queries the next line until the end of a line matches the beginning single and double quotation marks * concatenates the values of each line */
while (idx++ < lines.length - 1) {
line = lines[idx]
end = line.length - 1
// Check whether the end of the line matches the beginning with single or double quotation marks
if (line[end] === quoteChar) {
val += NEWLINE + line.substring(0, end)
break
}
// Concatenate the values
val += NEWLINE + line
}
// if single or double quoted, remove quotes
}
/** * If the current line is valid and ends with a single quotation mark or a double quotation mark, the value within the quotation mark */ is normally taken
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 whitespa
// If there are no single or double quotation marks at the beginning and end
val = val.trim()
}
// Obj is assigned to the matched key and value, and obj is returned
obj[key] = val
}
/** * If the current line does not conform to the writing rules and is in debug mode, an error log */ is given
else if (debug) {
// Remove the leading and trailing Spaces
const trimmedLine = line.trim()
// ignore empty and commented lines
// If the content is not empty and not a comment, log prompts
if (trimmedLine.length && trimmedLine[0]! = =The '#') {
log(`Failed to match key and value when parsing line ${idx + 1}: ${line}`)}}}return obj
}
Copy the code
The parse method parses the data that meets the rules into a key/value format. The overall logic is as follows:
- will
.env
The content of the - Each row of data is iterated and separated by regular expression
Subexpression 1
andSubexpression 2
Subexpression 1
saidkey
And thevalue
According toSubexpression 2
To determine whether to splice the next line of data:- if
Subexpression 2
If both single and double quotation marks are used,value
Take out theSubexpression 2
The part inside quotes - if
Subexpression 2
No single or double quotation marks at the beginning and endSubexpression 2
Value as thevalue
- if
Subexpression 2
If the value starts with a single or double quotation mark, but does not end with a matching quotation mark, the next line continues to be matched and concatenated. If the value is matched to or at the end, all matched values are concatenatedvalue
- if
More details are in the code comments, which you can read carefully.
Parse by row
Note the logic of dividing data by row, considering system compatibility, different system separators are not the same:
windows
System newline character\r\n
MacOS
The system drops a newline character\r
Unix
The system drops a newline character\n
const NEWLINES_MATCH = /\r\n|\n|\r/
Copy the code
Parse splits key/ Valude regular expression parsing
The key is to split the key/value regular expression and see how it is implemented:
const RE_INI_KEY_VAL = /^\s*([\w.-]+)\s*=\s*("[^"]*"|'[^']*'|[^#]*)? (\s*|\s*#.*)? $/
Copy the code
This re describes:
- Leading re
^\s*([\w.-]+)\s*=
It describes:- The beginning is
0-n
A blank space - It is followed by both English and Chinese underscores, dots and hyphens
- Come after
0-n
A blank space - And then there’s an equal sign
- The beginning is
- Intermediate regular
\s*("[^"]*"|'[^']*'|[^#]*)?
It describes:- This is followed by 0 to n Spaces
- followed
(Note 1)
: - The closing is in double quotes, and the middle is not in double quotes
0-n
A character- Or you can end with a single quote and have something else in between that is not a single quote
0-n
A character - Or something other than #
0-n
A character - The question mark at the end means
Note 1
All are optional
- Or you can end with a single quote and have something else in between that is not a single quote
- Final re
(\s*|\s*#.*)? $
It describes:- The following
0-n
A space or0-n
A space plus#
No. And0-n
Number of arbitrary characters
- The following
The idea of logging out
Log output in Dotenv is determined by user configuration, not by production/development environment. It’s more flexible
// debug Indicates the options parameter configuration
if (debug) {
log(`Failed to load ${dotenvPath} ${e.message}`)}Copy the code
According to the circumstances, it looks like this:
if(process.env.NODE_ENV ! = ='production') {
log('your log message.')}Copy the code
The specific use of that kind of log is up to different people depending on the actual situation.
conclusion
The overall implementation of dotenv library is not complex, relatively less code, very suitable for preliminary reading of the source partners. I am leng hammer, like small partners welcome to like collection ❤️❤️👍👍