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 defaultutf8Encoding format passesfs.readFileSyncreadThe root directoryUnder the.envfile
  • If the user uses a custom path, the configuration file from the defined path is read
  • callparseMethod will be.envData parsed intokey/valueIn the form of
  • Directly toprocess.envFor the assignment
  • If there are duplicates in assignmentkeyAccording 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.envThe content of the
  • Each row of data is iterated and separated by regular expressionSubexpression 1andSubexpression 2
  • Subexpression 1saidkeyAnd thevalueAccording toSubexpression 2To determine whether to splice the next line of data:
    • ifSubexpression 2If both single and double quotation marks are used,valueTake out theSubexpression 2The part inside quotes
    • ifSubexpression 2No single or double quotation marks at the beginning and endSubexpression 2Value as thevalue
    • ifSubexpression 2If 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

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:

  • windowsSystem newline character\r\n
  • MacOSThe system drops a newline character\r
  • UnixThe 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 is0-nA blank space
    • It is followed by both English and Chinese underscores, dots and hyphens
    • Come after0-nA blank space
    • And then there’s an equal sign
  • 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 quotes0-nA character
      • Or you can end with a single quote and have something else in between that is not a single quote0-nA character
      • Or something other than #0-nA character
      • The question mark at the end meansNote 1All are optional
  • Final re(\s*|\s*#.*)? $It describes:
    • The following0-nA space or0-nA space plus#No. And0-nNumber of arbitrary characters

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 ❤️❤️👍👍