parser.rb 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. require "dotenv/substitutions/variable"
  2. require "dotenv/substitutions/command" if RUBY_VERSION > "1.8.7"
  3. module Dotenv
  4. class FormatError < SyntaxError; end
  5. # This class enables parsing of a string for key value pairs to be returned
  6. # and stored in the Environment. It allows for variable substitutions and
  7. # exporting of variables.
  8. class Parser
  9. @substitutions =
  10. Substitutions.constants.map { |const| Substitutions.const_get(const) }
  11. LINE = /
  12. \A
  13. (?:export\s+)? # optional export
  14. ([\w\.]+) # key
  15. (?:\s*=\s*|:\s+?) # separator
  16. ( # optional value begin
  17. '(?:\'|[^'])*' # single quoted value
  18. | # or
  19. "(?:\"|[^"])*" # double quoted value
  20. | # or
  21. [^#\n]+ # unquoted value
  22. )? # value end
  23. (?:\s*\#.*)? # optional comment
  24. \z
  25. /x
  26. class << self
  27. attr_reader :substitutions
  28. def call(string)
  29. new(string).call
  30. end
  31. end
  32. def initialize(string)
  33. @string = string
  34. @hash = {}
  35. end
  36. def call
  37. @string.split("\n").each do |line|
  38. parse_line(line)
  39. end
  40. @hash
  41. end
  42. private
  43. def parse_line(line)
  44. if (match = line.match(LINE))
  45. key, value = match.captures
  46. @hash[key] = parse_value(value || "")
  47. elsif line.split.first == "export"
  48. if variable_not_set?(line)
  49. fail FormatError, "Line #{line.inspect} has an unset variable"
  50. end
  51. elsif line !~ /\A\s*(?:#.*)?\z/ # not comment or blank line
  52. fail FormatError, "Line #{line.inspect} doesn't match format"
  53. end
  54. end
  55. def parse_value(value)
  56. # Remove surrounding quotes
  57. value = value.strip.sub(/\A(['"])(.*)\1\z/, '\2')
  58. if Regexp.last_match(1) == '"'
  59. value = unescape_characters(expand_newlines(value))
  60. end
  61. if Regexp.last_match(1) != "'"
  62. self.class.substitutions.each do |proc|
  63. value = proc.call(value, @hash)
  64. end
  65. end
  66. value
  67. end
  68. def unescape_characters(value)
  69. value.gsub(/\\([^$])/, '\1')
  70. end
  71. def expand_newlines(value)
  72. value.gsub('\n', "\n")
  73. end
  74. def variable_not_set?(line)
  75. !line.split[1..-1].all? { |var| @hash.member?(var) }
  76. end
  77. end
  78. end