1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
|
require "dotenv/substitutions/variable"
require "dotenv/substitutions/command" if RUBY_VERSION > "1.8.7"
module Dotenv
# Error raised when encountering a syntax error while parsing a .env file.
class FormatError < SyntaxError; end
# Parses the `.env` file format into key/value pairs.
# It allows for variable substitutions, command substitutions, and exporting of variables.
class Parser
@substitutions = [
Dotenv::Substitutions::Variable,
Dotenv::Substitutions::Command
]
LINE = /
(?:^|\A) # beginning of line
\s* # leading whitespace
(?<export>export\s+)? # optional export
(?<key>[\w.]+) # key
(?: # optional separator and value
(?:\s*=\s*?|:\s+?) # separator
(?<value> # optional value begin
\s*'(?:\\'|[^'])*' # single quoted value
| # or
\s*"(?:\\"|[^"])*" # double quoted value
| # or
[^\#\n]+ # unquoted value
)? # value end
)? # separator and value end
\s* # trailing whitespace
(?:\#.*)? # optional comment
(?:$|\z) # end of line
/x
QUOTED_STRING = /\A(['"])(.*)\1\z/m
class << self
attr_reader :substitutions
def call(...)
new(...).call
end
end
def initialize(string, overwrite: false)
# Convert line breaks to same format
@string = string.gsub(/\r\n?/, "\n")
@hash = {}
@overwrite = overwrite
end
def call
@string.scan(LINE) do
match = $LAST_MATCH_INFO
if existing?(match[:key])
# Use value from already defined variable
@hash[match[:key]] = ENV[match[:key]]
elsif match[:export] && !match[:value]
# Check for exported variable with no value
if !@hash.member?(match[:key])
raise FormatError, "Line #{match.to_s.inspect} has an unset variable"
end
else
@hash[match[:key]] = parse_value(match[:value] || "")
end
end
@hash
end
private
# Determine if a variable is already defined and should not be overwritten.
def existing?(key)
!@overwrite && key != "DOTENV_LINEBREAK_MODE" && ENV.key?(key)
end
def parse_value(value)
# Remove surrounding quotes
value = value.strip.sub(QUOTED_STRING, '\2')
maybe_quote = Regexp.last_match(1)
# Expand new lines in double quoted values
value = expand_newlines(value) if maybe_quote == '"'
# Unescape characters and performs substitutions unless value is single quoted
if maybe_quote != "'"
value = unescape_characters(value)
self.class.substitutions.each { |proc| value = proc.call(value, @hash) }
end
value
end
def unescape_characters(value)
value.gsub(/\\([^$])/, '\1')
end
def expand_newlines(value)
if (@hash["DOTENV_LINEBREAK_MODE"] || ENV["DOTENV_LINEBREAK_MODE"]) == "legacy"
value.gsub('\n', "\n").gsub('\r', "\r")
else
value.gsub('\n', "\\\\\\n").gsub('\r', "\\\\\\r")
end
end
end
end
|