Ctrl K
ComunidadGuías…

app/services/env/interpolation.rb

Rama develop · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
module Env
  # Resolves `$VAR` / `${VAR}` references inside .env values the way Docker
  # Compose does when it reads the file.
  #
  # This matters on import: a donor `.env` written by hand or by the old
  # configurator carries the *literal* text, but the containers only ever saw
  # the expanded result. A token like `abc$xyz` reaches InfluxDB as `abc`,
  # because `$xyz` is undefined and expands to nothing. Importing the literal
  # would persist a secret the stack never actually used — and HELIOS quotes it
  # on export, so the value suddenly changes for every recreated service (#377).
  #
  # References resolve against the variables defined earlier in the same file;
  # anything else expands to an empty string, mirroring Compose's "variable is
  # not set. Defaulting to a blank string". The environment of the HELIOS
  # process is deliberately not consulted — it is not the shell the user's
  # stack was started from.
  #
  # It lives under Env:: rather than Compose:: because the variable table is
  # always a .env file, and the backslash dialect `resolve_escaped` implements
  # exists only there — compose.yaml carries YAML quoting instead. The importer
  # reuses it for compose.yaml strings, which docker likewise resolves against
  # .env, so `${VAR}` has one meaning across HELIOS.
  module Interpolation
    NAME = /[A-Za-z_][A-Za-z0-9_]*/

    # The argument of `${VAR:-default}` is a value in its own right and may
    # nest further references, so it has to match balanced braces recursively —
    # `${DB_DATA:-${BASE}/postgres}` is one reference with a default, not a
    # reference to BASE wrapped in stray text.
    REFERENCE = /
      \$(?:
        \$                                                       # $$ — escaped literal $
        |
        \{(?<name>#{NAME})(?<operator>:?[-+?])?(?<argument>(?:[^{}]|\{\g<argument>\})*)\}
        |
        (?<bare>#{NAME})
      )
    /x

    # Inside double quotes Compose also processes backslash escapes, and it does
    # so in the same left-to-right pass: `\$` is a literal dollar that starts no
    # reference. Unescaping first and expanding afterwards would turn
    # `"\${HOME}"` into the value of HOME instead of the text `${HOME}`.
    ESCAPED_REFERENCE = /(?<escape>\\.)|#{REFERENCE}/m

    # Backslash sequences Compose expands inside double quotes; any other
    # escaped character stands for itself. Env::File inverts this table to
    # write those characters back out, so an entry added here keeps both
    # directions in step on its own.
    ESCAPE_SEQUENCES = { 'n' => "\n", 'r' => "\r", 't' => "\t" }.freeze

    module_function

    # For unquoted values, where a backslash is literal.
    def resolve(value, variables)
      return value unless value.include?('$')

      substitute(value, REFERENCE, variables)
    end

    # For the body of a double-quoted value, where escapes and references share
    # one pass.
    def resolve_escaped(value, variables)
      return value unless value.match?(/[$\\]/)

      substitute(value, ESCAPED_REFERENCE, variables)
    end

    # Names referenced by a string, for callers that need to know what a value
    # depends on rather than what it expands to. `$$` references nothing.
    # Names inside a default count too: `${DATA:-${BASE}/x}` depends on both,
    # and a caller that only saw BASE would drop DATA as unreferenced.
    def references(value)
      value.to_s.scan(REFERENCE).flat_map do |name, _operator, argument, bare|
        [name || bare, *references(argument)].compact
      end
    end

    def substitute(value, pattern, variables)
      value.gsub(pattern) do
        capture = Regexp.last_match.named_captures

        if (escape = capture['escape'])
          ESCAPE_SEQUENCES.fetch(escape[1], escape[1])
        elsif (bare = capture['bare'])
          variables[bare].to_s
        elsif (name = capture['name'])
          expand(variables[name], capture['operator']) do
            substitute(capture['argument'].to_s, pattern, variables)
          end
        else # `$$` — an escaped literal dollar
          '$'
        end
      end
    end

    # `${VAR-default}` substitutes when the variable is unset, `${VAR:-default}`
    # also when it is empty; `${VAR+alt}` / `${VAR:+alt}` do the opposite.
    # "Empty" is the empty string and nothing else — a value of whitespace is
    # set as far as Compose is concerned, so Rails' blankness must not stand in
    # for it, or both operators pick the wrong branch.
    # `${VAR:?message}` makes Compose abort — there is no stack to abort here,
    # so the reference falls back to the empty string like any unset variable.
    #
    # The argument arrives as a block because it is a value in its own right:
    # Compose interpolates the default it falls back to, so `${A:-$B}` yields
    # the contents of B, not the text `$B`. Only the branch that uses the
    # argument pays for that pass.
    def expand(value, operator)
      set = operator&.start_with?(':') ? !value.to_s.empty? : !value.nil?

      case operator&.delete_prefix(':')
      when '-' then set ? value : yield
      when '+' then set ? yield : ''
      else value.to_s
      end
    end
  end
end