1
0

parser.rb 3.1 KB

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