require 'augeas' if Puppet.features.augeas?

# Base Augeas provider
# Handles basics such as opening, accessing and saving changes via an Augeas
# handle, plus standard configuration from a Puppet resource (e.g. the `target`
# parameter).
#
# To use, include as parent provider:
#
#     Puppet::Type.type(:example).provide(
#       :augeas,
#       :parent => Puppet::Type.type(:augeasprovider).provider(:default)
#     ) do
#       # [..]
#     end
#
Puppet::Type.type(:augeasprovider).provide(:default) do
  # Class methods automatically added to a Puppet provider by including the
  # {AugeasProviders::Provider} mixin.

  # Returns the Augeas version used
  #
  # @return [String] Augeas version in use
  # @api public
  def self.aug_version
    @aug_version ||= Augeas.open(nil, nil, Augeas::NO_MODL_AUTOLOAD) { |aug| aug.get('/augeas/version') }
  end

  # Returns whether a feature is supported.
  #
  # The following features are currently supported:
  #
  # * `:regexpi`: whether Augeas supports an 'i' flag in regexp expressions
  # * `:post_resource_eval`: whether Puppet supports `post_resource_eval` hooks
  #
  # @param [Symbol] feature the feature to check
  # @return [Boolean] whether feature is supported
  # @api public
  def self.supported?(feature)
    case feature
    when :regexpi
      Puppet::Util::Package.versioncmp(aug_version, '1.0.0') >= 0
    when :post_resource_eval
      Puppet::Util::Package.versioncmp(Puppet.version, '3.4.0') >= 0
    else
      raise Puppet::Error, "Unknown feature '#{feature}'"
    end
  end

  # Returns a node label to use for creating a new entry in an Augeas sequence
  # (seq), given the current list, e.g. '1' if it's the first.  Supply
  # aug.match('$target/*') or similar.
  #
  # @param [Array<String>] existing paths, from Augeas#match
  # @return [String] new node label
  def self.next_seq(matches)
    last = matches.map { |p| path_label(nil, p).to_i }.max || 0
    (last + 1).to_s
  end

  # Returns an Augeas handler.
  #
  # On Puppet >= 3.4, stores and returns a shared Augeas handler
  # for all instances of the class
  #
  # @return [Augeas] Augeas shared Augeas handle
  # @api private
  def self.aug_handler
    if supported?(:post_resource_eval)
      @aug ||= Augeas.open(nil, loadpath, Augeas::NO_MODL_AUTOLOAD)
    else
      Augeas.open(nil, loadpath, Augeas::NO_MODL_AUTOLOAD)
    end
  end

  # Close the shared Augeas handler.
  #
  # @param [Augeas] aug open Augeas handle
  # @api public
  def self.augclose!(aug)
    aug.close
  end

  # Opens Augeas and returns a handle to use.  It loads only the file
  # identified by {#target} (and the supplied `resource`) using {#lens}.
  #
  # If called with a block, this will be yielded to and the Augeas handle
  # closed after the block has executed (on Puppet < 3.4.0).
  # Otherwise, the handle will be returned and not closed automatically.
  # On Puppet >= 3.4, the handle will be closed by `post_resource_eval`.
  # On older versions, the caller is responsible for closing it to free
  # resources.
  #
  # If `yield_resource` is set to true, the supplied `resource` will be passed
  # as a yieldparam to the block, after the `aug` handle. Any arguments passed
  # after `yield_resource` will be added as yieldparams to the block.
  #
  # @param [Puppet::Resource] resource resource being evaluated
  # @param [Boolean] yield_resource whether to send `resource` as a yieldparam
  # @param [Splat] yield_params a splat of parameters to pass as yieldparams if `yield_resource` is true
  # @return [Augeas] Augeas handle if no block is given
  # @yield [aug, resource, *yield_params] block that uses the Augeas handle
  # @yieldparam [Augeas] aug open Augeas handle
  # @yieldparam [Puppet::Resource] resource the supplied Puppet resource, passed if `yield_resource` is set to true
  # @yieldparam [Splat] *yield_params a splat of additional arguments sent to the block, if `yield_resource` is set to true
  # @raise [Puppet::Error] if Augeas did not load the file
  # @api public
  def self.augopen(resource = nil, yield_resource = false, *yield_params, &block)
    augopen_internal(resource, false, yield_resource, *yield_params, &block)
  end

  # Opens Augeas and returns a handle to use.  It loads only the file
  # for the current Puppet resource using {AugeasProviders::Provider::ClassMethods#lens}.
  # #augsave! is called after the block is evaluated.
  #
  # If called with a block, this will be yielded to and the Augeas handle
  # closed after the block has executed (on Puppet < 3.4.0).
  # Otherwise, the handle will be returned and not closed automatically.
  # On Puppet >= 3.4, the handle will be closed by `post_resource_eval`.
  # On older versions, the caller is responsible for closing it to free
  # resources.
  #
  # If `yield_resource` is set to true, the supplied `resource` will be passed
  # as a yieldparam to the block, after the `aug` handle. Any arguments passed
  # after `yield_resource` will be added as yieldparams to the block.
  #
  # @param [Puppet::Resource] resource resource being evaluated
  # @param [Boolean] yield_resource whether to send `resource` as a yieldparam
  # @param [Splat] yield_params a splat of parameters to pass as yieldparams if `yield_resource` is true
  # @return [Augeas] Augeas handle if no block is given
  # @yield [aug, resource, *yield_params] block that uses the Augeas handle
  # @yieldparam [Augeas] aug open Augeas handle
  # @yieldparam [Puppet::Resource] resource the supplied Puppet resource, passed if `yield_resource` is set to true
  # @yieldparam [Splat] *yield_params a splat of additional arguments sent to the block, if `yield_resource` is set to true
  # @raise [Puppet::Error] if Augeas did not load the file
  # @api public
  def self.augopen!(resource = nil, yield_resource = false, *yield_params, &block)
    augopen_internal(resource, true, yield_resource, *yield_params, &block)
  end

  # Saves all changes made in the current Augeas handle and checks for any
  # errors while doing so.
  # Reloads the tree afterwards to remove specific changes for next resource.
  #
  # @param [Augeas] aug open Augeas handle
  # @param [Boolean] reload whether to reload the tree after saving
  # @raise [Augeas::Error] if saving fails
  # @api public
  def self.augsave!(aug, reload = false)
    aug.save!
  rescue Augeas::Error
    errors = []
    aug.match('/augeas//error').each do |errnode|
      aug.match("#{errnode}/*").each do |subnode|
        subvalue = aug.get(subnode)
        errors << "#{subnode} = #{subvalue}"
      end
    end
    debug("Save failure details:\n" + errors.join("\n"))
    raise Augeas::Error, 'Failed to save Augeas tree to file. See debug logs for details.'
  ensure
    aug.load! if reload
  end

  # Define a method with a block passed to #augopen
  #
  # @param [Symbol] method the name of the method to create
  # @yield [aug, resource, *args] block that uses the Augeas handle
  # @yieldparam [Augeas] aug open Augeas handle
  # @yieldparam [Puppet::Resource] resource the supplied Puppet resource
  # @yieldparam [Splat] *args a splat of additional arguments sent to the block
  # @api public
  def self.define_aug_method(method, &block)
    define_method(method) do |*args|
      # We are calling the resource's augopen here, not the class
      augopen(true, *args, &block)
    end
  end

  # Define a method with a block passed to #augopen!
  #
  # @param [Symbol] method the name of the method to create
  # @yield [aug, resource, *args] block that uses the Augeas handle
  # @yieldparam [Augeas] aug open Augeas handle
  # @yieldparam [Puppet::Resource] resource the supplied Puppet resource
  # @yieldparam [Splat] *args a splat of additional arguments sent to the block
  # @api public
  def self.define_aug_method!(method, &block)
    define_method(method) do |*args|
      # We are calling the resource's augopen! here, not the class
      augopen!(true, *args, &block)
    end
  end

  # Defines a property getter with a provided implementation.  It works from
  # a node identified with the given `label` beneath the resource.
  #
  # Supports three implementations based on the type specified:
  #
  # :string causes the getter to return the value of the node below
  # resource with the label given in opts
  #
  # :array causes the getter to return an array of values matching the label.
  # If sublabel is given, values of matching nodes beneath the
  # label node will be returned in an array.  If sublabel is :seq, values of
  # nodes matching a numbered seq will be returned.
  #
  # :hash causes the getter to return a hash of the value of each matching
  # label node against the value of each sublabel node.
  #
  # @param [String] name the name of the property
  # @param [Hash] opts the options to create the setter
  # @option opts [String] label node label to match beneath resource, default is `name.to_s`. When the value is `:resource`, `$resource` will be used as the path to the node
  # @option opts [Symbol] type either :string, :array or :hash
  # @option opts [String] default default value for hash values if sublabel doesn't exist
  # @option opts [String] sublabel label of next node(s) beneath node label, used in array and hash values, or :seq for array values representing a numbered seq
  # @api public
  def self.attr_aug_reader(name, opts = {})
    label = opts[:label] || name.to_s
    default = opts[:default] || nil
    type = opts[:type] || :string
    sublabel = opts[:sublabel] || nil
    split_by = opts[:split_by] || nil

    rpath = (label == :resource) ? '$resource' : "$resource/#{label}"

    if type == :hash && sublabel.nil?
      raise(Puppet::Error, 'You must provide a sublabel for type hash')
    end

    unless [:string, :array, :hash].include? type
      raise(Puppet::Error, "Invalid type: #{type}")
    end

    # Class getter method using an existing aug handler
    # Emulate define_singleton_method for Ruby 1.8
    metaclass = class << self; self; end
    metaclass.send(:define_method, "attr_aug_reader_#{name}") do |aug, *_args|
      case type
      when :string
        aug.get(rpath)
      when :array
        return (aug.get(rpath) || '').split(split_by) if split_by
        aug.match(rpath).map { |p|
          if sublabel.nil?
            aug.get(p)
          else
            sp = (sublabel == :seq) ? "#{p}/*[label()=~regexp('[0-9]+')]" : "#{p}/#{sublabel}"
            aug.match(sp).map { |spp| aug.get(spp) }
          end
        }.flatten
      when :hash
        values = {}
        aug.match(rpath).each do |p|
          sp = "#{p}/#{sublabel}"
          values[aug.get(p)] = aug.get(sp) || default
        end
        values
      end
    end

    # Instance getter method for the instance
    define_method("attr_aug_reader_#{name}") do |aug, *args|
      self.class.send("attr_aug_reader_#{name}", aug, *args)
    end

    # We are calling the resource's augopen here, not the class
    define_method(name) do |*args|
      augopen do |aug|
        send("attr_aug_reader_#{name}", aug, *args)
      end
    end
  end

  # Defines a property setter using #augopen
  #
  # @param [String] name the name of the property
  # @param [Hash] opts the options to create the setter
  # @option opts [String] label node label to match beneath resource, default is `name.to_s`. When the value is `:resource`, `$resource` will be used as the path to the node
  # @option opts [Symbol] type either :string, :array or :hash
  # @option opts [String] default default value for hash values if sublabel doesn't exist
  # @option opts [String] sublabel label of next node(s) beneath node label, used in array and hash values, or :seq for array values representing a numbered seq
  # @option opts [Boolean] purge_ident whether to purge other matches (keeps the last one only)
  # @option opts [Boolean] rm_node whether setting a string value to `nil` removes the node (default is to clear its value)
  # @api public
  def self.attr_aug_writer(name, opts = {})
    label = opts[:label] || name.to_s
    default = opts[:default] || nil
    type = opts[:type] || :string
    sublabel = opts[:sublabel] || nil
    purge_ident = opts[:purge_ident] || false
    rm_node = opts[:rm_node] || false
    split_by = opts[:split_by] || nil

    rpath = (label == :resource) ? '$resource' : "$resource/#{label}"

    if type == :hash && sublabel.nil?
      raise(Puppet::Error, 'You must provide a sublabel for type hash')
    end

    unless [:string, :array, :hash].include? type
      raise(Puppet::Error, "Invalid type: #{type}")
    end

    # Class setter method using an existing aug handler
    # Emulate define_singleton_method for Ruby 1.8
    metaclass = class << self; self; end
    metaclass.send(:define_method, "attr_aug_writer_#{name}") do |aug, *args|
      aug.rm("#{rpath}[position() != 1]") if purge_ident
      case type
      when :string
        if args[0]
          aug.set(rpath, args[0].to_s)
        elsif rm_node
          aug.rm(rpath)
        else
          aug.clear(rpath)
        end
      when :array
        if args[0].nil? || args[0].empty?
          aug.rm(rpath)
        elsif split_by
          aug.set(rpath, args[0].join(split_by))
        elsif sublabel.nil?
          aug.rm(rpath)
          count = 0
          args[0].each do |v|
            count += 1
            aug.set("#{rpath}[#{count}]", v)
          end
        elsif sublabel == :seq
          # Make sure only our values are used
          aug.rm("#{rpath}/*[label()=~regexp('[0-9]+')]")
          count = 0
          args[0].each do |v|
            count += 1
            aug.set("#{rpath}/#{count}", v)
          end
        else
          # Make sure only our values are used
          aug.rm("#{rpath}/#{sublabel}")
          count = 0
          args[0].each do |v|
            count += 1
            aug.set("#{rpath}/#{sublabel}[#{count}]", v)
          end
        end
      when :hash
        # First get rid of all entries
        aug.rm(rpath)
        args[0].each do |k, v|
          aug.set("#{rpath}[.='#{k}']", k)
          unless v == default
            aug.set("#{rpath}[.='#{k}']/#{sublabel}", v)
          end
        end
      end
    end

    # Instance setter method for the instance
    define_method("attr_aug_writer_#{name}") do |aug, *args|
      self.class.send("attr_aug_writer_#{name}", aug, *args)
    end

    # We are calling the resource's augopen here, not the class
    define_method("#{name}=") do |*args|
      augopen! do |aug|
        send("attr_aug_writer_#{name}", aug, *args)
      end
    end
  end

  # Define getter and setter for a property
  #
  # @param [Symbol] name the name of the property
  # @param [Hash] opts the options to create the setter
  # @option opts [String] label node label to match beneath resource, default is `name.to_s`. When the value is `:resource`, `$resource` will be used as the path to the node
  # @option opts [Symbol] type either :string, :array or :hash
  # @option opts [String] default default value for hash values if sublabel doesn't exist
  # @option opts [String] sublabel label of next node(s) beneath node label, used in array and hash values, or :seq for array values representing a numbered seq
  # @option opts [Boolean] purge_ident whether to purge other matches (keeps the last one only)
  # @api public
  def self.attr_aug_accessor(name, opts = {})
    attr_aug_reader(name, opts)
    attr_aug_writer(name, opts)
  end

  # Setter for the default file path managed by the provider.
  #
  # Takes a block to store, but doesn't yield.  Will be called when it's
  # needed.
  #
  # @yield block that identifies the default file path managed by the provider
  # @yieldreturn [String] default file path
  # @api public
  def self.default_file(&block)
    @default_file_block = block
  end

  # Getter and setter for the Augeas lens used for this provider.
  #
  # When called with a block, will only store the block - it doesn't yield.
  #
  # When called without a block, expects `resource` parameter which is
  # passed into the block, which returns the lens to be used.
  #
  # @param resource [Puppet::Resource] required for getter, resource being evaluated
  # @yield [resource] block that identifies the lens to use
  # @yieldparam [Puppet::Resource] resource resource being evaluted
  # @yieldreturn [String] Augeas lens to use, e.g. `'Hosts.lns'`
  # @return [String] Augeas lens to use, e.g. `'Hosts.lns'`
  # @raise [Puppet::Error] if no block has been set when getting
  # @api public
  def self.lens(resource = nil, &block)
    if block
      @lens_block = block
    else
      raise(Puppet::Error, 'Lens is not provided') unless @lens_block
      @lens_block.call(resource)
    end
  end

  # Wrapper around aug.label for older versions of Augeas
  # and values not found in the tree.
  #
  # @param [Augeas] aug Augeas handler
  # @param [String] path expression to get the label from
  # @return [String] label of the given path
  # @api public
  def self.path_label(aug, path)
    if aug.respond_to? :label
      label = aug.label(path)
    end

    # Fallback
    label || path.split('/')[-1].split('[')[0]
  end

  # Determine which quote is needed
  #
  # @param [String] value the value to quote
  # @param [String] oldvalue the optional old value, used to auto-detect existing quoting
  # @return [String] the quoted value
  # @api public
  def self.whichquote(value, resource = nil, oldvalue = nil)
    oldquote = readquote oldvalue

    quote = if resource&.parameters&.include?(:quoted)
              resource[:quoted]
            else
              :auto
            end

    if quote == :auto
      quote = if oldquote
                oldquote
              elsif value.match?(%r{[|&;()<>\s]})
                :double
              else
                :none
              end
    end

    case quote
    when :double
      '"'
    when :single
      "'"
    else
      ''
    end
  end

  # Automatically quote a value
  #
  # @param [String] value the value to quote
  # @param [String] oldvalue the optional old value, used to auto-detect existing quoting
  # @return [String] the quoted value
  # @api public
  def self.quoteit(value, resource = nil, oldvalue = nil)
    quote = whichquote(value, resource, oldvalue)
    "#{quote}#{value}#{quote}"
  end

  # Detect what type of quoting a value uses
  #
  # @param [String] value the value to be analyzed
  # @return [Symbol] the type of quoting used (:double, :single or nil)
  # @api public
  def self.readquote(value)
    if value =~ %r{^(["'])(.*)(?:\1)$}
      case Regexp.last_match(1)
      when '"' then :double
      when "'" then :single
      else nil
      end
    else
      nil
    end
  end

  # Getter and setter for the Augeas path expression representing an
  # individual resource inside a file, that's managed by this provider.
  #
  # When called with a block, will only store the block - it doesn't yield.
  # The block is later used to generate the path expression.
  #
  # When called without a block, expects `resource` parameter which is
  # passed into the block, which returns the path expression representing
  # the supplied resource.
  #
  # If no block has already been set, it returns the path expression
  # representing the top-level of the file.
  #
  # @param resource [Puppet::Resource] required for getter, resource being evaluated
  # @yield [resource] block that identifies the path expression
  # @yieldparam [Puppet::Resource] resource resource being evaluted
  # @yieldreturn [String] Augeas path expression, e.g. `'/files/etc/hosts/1'`
  # @return [String] Augeas path expression to use, e.g. `'/files/etc/hosts/1'`
  # @raise [Puppet::Error] if no default file block is set and no resource is passed
  # @see AugeasProviders::Provider#resource_path
  # @see #target
  # @api public
  def self.resource_path(resource = nil, &block)
    if block
      @resource_path_block = block
    elsif @resource_path_block
      @resource_path_block.call(resource)
    else
      "#{target(resource)}/#{resource[:name]}"
    end
  end

  # Sets useful Augeas variables for the session.
  #
  # * `$target` points to the root of the target file
  # * `$resource` points to path defined by #resource_path
  #
  # It also sets `/augeas/context` to the target file so
  # relative paths can be used, before the variables are set.
  #
  # If supplied with a resource, it will be used to determine the
  # path to the used file.
  #
  # @param [Augeas] aug Augeas handle
  # @param [Puppet::Resource] resource resource being evaluated
  # @see #resource_path
  # @api public
  def self.setvars(aug, resource = nil)
    aug.context = "/files#{target(resource)}"
    aug.defnode('target', "/files#{target(resource)}", nil)
    aug.defvar('resource', resource_path(resource)) if resource
  end

  # Gets the path expression representing the file being managed.
  #
  # If supplied with a resource, this will represent the file identified by
  # the resource, else the default file that the provider manages.
  #
  # @param [Puppet::Resource] resource resource being evaluated
  # @return [String] path expression representing the file being managed
  # @raise [Puppet::Error] if no default block is set and no resource is passed
  # @see AugeasProviders::Provider#target
  # @see #resource_path
  # @api public
  def self.target(resource = nil)
    file = @default_file_block.call if @default_file_block
    file = resource[:target] if resource && resource[:target]
    raise(Puppet::Error, 'No target file given') if file.nil?
    file.chomp('/')
  end

  # Automatically unquote a value
  #
  # @param [String] value the value to unquote
  # @return [String] the unquoted value
  # @api public
  def self.unquoteit(value)
    if value =~ %r{^(["'])(.*)(?:\1)$}
      Regexp.last_match(2)
    else
      value
    end
  end

  # Returns whether text is parsed as path using lens
  #
  # @param [String] text the text to test
  # @param [String] path the relative path to test
  # @param [String] lens the lens to use for parsing
  # @return [Boolean] whether the path was found when parsing text with lens
  # @api public
  def self.parsed_as?(text, path, lens)
    Augeas.open(nil, nil, Augeas::NO_MODL_AUTOLOAD) do |aug|
      if aug.respond_to? :text_store
        aug.set('/input', text)
        if aug.text_store(lens, '/input', '/parsed')
          return aug.match("/parsed/#{path}").any?
        end
      else
        # ruby-augeas < 0.5 doesn't support text_store
        Tempfile.open('aug_text_store') do |tmpfile|
          tmpfile.write(text)
          tmpfile.flush
          aug.transform(
            lens: lens,
            name: 'Text_store',
            incl: tmpfile.path.to_s,
            excl: [],
          )
          aug.load!
          return aug.match("/files#{tmpfile.path}/#{path}").any?
        end
      end
    end
    false
  end

  # Sets the post_resource_eval class hook for Puppet
  # This is only used with Puppet > 3.4.0
  # and allows to clean the shared Augeas handler.
  def self.post_resource_eval
    augclose!(aug_handler)
    @aug = nil
  end

  # Returns a set of load paths to use when initialising Augeas.
  #
  # @return [String] colon-separated string of module paths, or nil if defaults are to be used
  def self.loadpath
    loadpath = nil
    plugins = File.join(Puppet[:libdir], 'augeas', 'lenses')
    if File.exist?(plugins)
      loadpath = loadpath.to_s.split(File::PATH_SEPARATOR).push(plugins).join(File::PATH_SEPARATOR)
    end
    loadpath
  end

  # Opens Augeas and returns a handle to use.  It loads only the file
  # identified by {#target} (and the supplied `resource`) using {#lens}.
  #
  # If called with a block, this will be yielded to and the Augeas handle
  # closed after the block has executed (on Puppet < 3.4.0).
  # Otherwise, the handle will be returned and not closed automatically.
  # On Puppet >= 3.4, the handle will be closed by `post_resource_eval`.
  # On older versions, the caller is responsible for closing it to free
  # resources.
  #
  # If `yield_resource` is set to true, the supplied `resource` will be passed
  # as a yieldparam to the block, after the `aug` handle. Any arguments passed
  # after `yield_resource` will be added as yieldparams to the block.
  #
  # @param [Puppet::Resource] resource resource being evaluated
  # @param [Boolean] autosave whether to call augsave! automatically after the block evaluation
  # @param [Boolean] yield_resource whether to send `resource` as a yieldparam
  # @param [Splat] yield_params a splat of parameters to pass as yieldparams if `yield_resource` is true
  # @return [Augeas] Augeas handle if no block is given
  # @yield [aug, resource, *yield_params] block that uses the Augeas handle
  # @yieldparam [Augeas] aug open Augeas handle
  # @yieldparam [Puppet::Resource] resource the supplied Puppet resource, passed if `yield_resource` is set to true
  # @yieldparam [Splat] *yield_params a splat of additional arguments sent to the block, if `yield_resource` is set to true
  # @raise [Puppet::Error] if Augeas did not load the file
  # @api private
  def self.augopen_internal(resource = nil, autosave = false, yield_resource = false, *yield_params, &block)
    aug = aug_handler
    file = target(resource)
    begin
      lens_name = lens[%r{[^\.]+}]
      if aug.match("/augeas/load/#{lens_name}").empty?
        aug.transform(
          lens: lens,
          name: lens_name,
          incl: file,
          excl: [],
        )
        aug.load!
      elsif aug.match("/augeas/load/#{lens_name}/incl[.='#{file}']").empty?
        # Only add missing file
        aug.set("/augeas/load/#{lens_name}/incl[.='#{file}']", file)
        aug.load!
      end

      if File.exist?(file) && aug.match("/files#{file}").empty?
        message = aug.get("/augeas/files#{file}/error/message")
        unless aug.match("/augeas/files#{file}/error/pos").empty?
          line = aug.get("/augeas/files#{file}/error/line")
          char = aug.get("/augeas/files#{file}/error/char")
          message += " (line:#{line}, character:#{char})"
        end
        from = loadpath.nil? ? '' : " from #{loadpath}"
        raise(Puppet::Error, "Augeas didn't load #{file} with #{lens}#{from}: #{message}")
      end

      if block
        setvars(aug, resource)
        if yield_resource
          block.call(aug, resource, *yield_params)
        else
          block.call(aug)
        end
      else
        aug
      end
    rescue
      autosave = false
      raise
    ensure
      if aug && block && !supported?(:post_resource_eval)
        augsave!(aug) if autosave
        augclose!(aug)
      end
    end
  end

  # Returns the Augeas version used
  #
  # @return [String] Augeas version in use
  # @api public
  def aug_version
    self.class.aug_version
  end

  # Returns whether a feature is supported.
  #
  # The following features are currently supported:
  #
  # * `:regexpi`: whether Augeas supports an 'i' flag in regexp expressions
  # * `:post_resource_eval`: whether Puppet supports `post_resource_eval` hooks
  #
  # @param [Symbol] feature the feature to check
  # @return [Boolean] whether feature is supported
  # @api public
  def supported?(feature)
    self.class.supported?(feature)
  end

  # Opens Augeas and returns a handle to use.  It loads only the file
  # for the current Puppet resource using {AugeasProviders::Provider::ClassMethods#lens}.
  #
  # If called with a block, this will be yielded to and the Augeas handle
  # closed after the block has executed (on Puppet < 3.4.0).
  # Otherwise, the handle will be returned and not closed automatically.
  # On Puppet >= 3.4, the handle will be closed by `post_resource_eval`.
  # On older versions, the caller is responsible for closing it to free
  # resources.
  #
  # If `yield_resource` is set to true, the supplied `resource` will be passed
  # as a yieldparam to the block, after the `aug` handle. Any arguments passed
  # after `yield_resource` will be added as yieldparams to the block.
  #
  # @return [Augeas] Augeas handle if no block is given
  # @yield [aug, resource, *yield_params] block that uses the Augeas handle
  # @yieldparam [Augeas] aug open Augeas handle
  # @yieldparam [Puppet::Resource] resource the supplied Puppet resource, passed if `yield_resource` is set to true
  # @yieldparam [Splat] *yield_params a splat of additional arguments sent to the block, if `yield_resource` is set to true
  # @raise [Puppet::Error] if Augeas did not load the file
  # @api public
  def augopen(yield_resource = false, *yield_params, &block)
    self.class.augopen(resource, yield_resource, *yield_params, &block)
  end

  # Opens Augeas and returns a handle to use.  It loads only the file
  # for the current Puppet resource using {AugeasProviders::Provider::ClassMethods#lens}.
  # #augsave! is called after the block is evaluated.
  #
  # If called with a block, this will be yielded to and the Augeas handle
  # closed after the block has executed (on Puppet < 3.4.0).
  # Otherwise, the handle will be returned and not closed automatically.
  # On Puppet >= 3.4, the handle will be closed by `post_resource_eval`.
  # On older versions, the caller is responsible for closing it to free
  # resources.
  #
  # @return [Augeas] Augeas handle if no block is given
  # @yield [aug, resource, *yield_params] block that uses the Augeas handle
  # @yieldparam [Augeas] aug open Augeas handle
  # @yieldparam [Puppet::Resource] resource the supplied Puppet resource, passed if `yield_resource` is set to true
  # @yieldparam [Splat] *yield_params a splat of additional arguments sent to the block, if `yield_resource` is set to true
  # @raise [Puppet::Error] if Augeas did not load the file
  # @api public
  def augopen!(yield_resource = false, *yield_params, &block)
    self.class.augopen!(resource, yield_resource, *yield_params, &block)
  end

  # Saves all changes made in the current Augeas handle and checks for any
  # errors while doing so.
  #
  # @param [Augeas] aug open Augeas handle
  # @param [Boolean] reload whether to reload the tree after saving
  # @raise [Augeas::Error] if saving fails
  # @api public
  def augsave!(aug, reload = false)
    self.class.augsave!(aug, reload)
  end

  # Close the shared Augeas handler.
  #
  # @param [Augeas] aug open Augeas handle
  # @api public
  def augclose!(aug)
    self.class.augclose!(aug)
  end

  # Returns an Augeas handler.
  #
  # On Puppet >= 3.4, stores and returns a shared Augeas handler
  # for all instances of the class
  #
  # @return [Augeas] Augeas shared Augeas handle
  # @api private
  def aug_handler
    self.class.aug_handler
  end

  # Wrapper around Augeas#label for older versions of Augeas
  #
  # @param [Augeas] aug Augeas handler
  # @param [String] path expression to get the label from
  # @return [String] label of the given path
  # @api public
  def path_label(aug, path)
    self.class.path_label(aug, path)
  end

  # Determine which quote is needed
  #
  # @param [String] value the value to quote
  # @param [String] oldvalue the optional old value, used to auto-detect existing quoting
  # @return [String] the quoted value
  # @api public
  def whichquote(value, oldvalue = nil)
    self.class.whichquote(value, resource, oldvalue)
  end

  # Automatically quote a value
  #
  # @param [String] value the value to quote
  # @param [String] oldvalue the optional old value, used to auto-detect existing quoting
  # @return [String] the quoted value
  # @api public
  def quoteit(value, oldvalue = nil)
    self.class.quoteit(value, resource, oldvalue)
  end

  # Detect what type of quoting a value uses
  #
  # @param [String] value the value to be analyzed
  # @return [Symbol] the type of quoting used (:double, :single or nil)
  # @api public
  def readquote(value)
    self.class.readquote(value)
  end

  # Gets the Augeas path expression representing the individual resource inside
  # the file, that represents the current Puppet resource.
  #
  # If no block was set by the provider's class method, it returns the path
  # expression representing the top-level of the file.
  #
  # @return [String] Augeas path expression to use, e.g. `'/files/etc/hosts/1'`
  # @see AugeasProviders::Provider::ClassMethods#resource_path
  # @see #target
  # @api public
  def resource_path
    self.class.resource_path(resource)
  end

  # Sets useful Augeas variables for the session:
  #
  # * `$target` points to the root of the target file
  # * `$resource` points to path defined by #resource_path
  #
  # It also sets `/augeas/context` to the target file so
  # relative paths can be used, before the variables are set.
  #
  # If supplied with a resource, it will be used to determine the
  # path to the used file.
  #
  # @param [Augeas] aug Augeas handle
  # @see #resource_path
  # @api public
  def setvars(aug)
    self.class.setvars(aug, resource)
  end

  # Gets the path expression representing the file being managed for the
  # current Puppet resource.
  #
  # @return [String] path expression representing the file being managed
  # @see AugeasProviders::Provider::ClassMethods#target
  # @see #resource_path
  # @api public
  def target
    self.class.target(resource)
  end

  # Automatically unquote a value
  #
  # @param [String] value the value to unquote
  # @return [String] the unquoted value
  # @api public
  def unquoteit(value)
    self.class.unquoteit(value)
  end

  # Returns whether text is parsed as path using lens
  #
  # @param [String] text the text to test
  # @param [String] path the relative path to test
  # @param [String] lens the lens to use for parsing
  # @return [Boolean] whether the path was found when parsing text with lens
  # @api public
  def parsed_as?(text, path, lens = nil)
    lens ||= self.class.lens(resource)
    self.class.parsed_as?(text, path, lens)
  end

  # Default method to determine the existence of a resource
  # can be overridden if necessary
  def exists?
    augopen do |aug|
      !aug.match('$resource').empty?
    end
  end

  # Default method to destroy a resource
  # can be overridden if necessary
  def destroy
    augopen! do |aug|
      aug.rm('$resource')
    end
  end

  # Default method to flush a resource
  #
  # On Puppet >= 3.4, this takes care of
  # saving the tree for the shared Augeas handler.
  #
  # This method can be overridden in your provider
  # but you should make sure to call `super`
  # to ensure that the tree will be saved in Puppet >= 3.4
  def flush
    augsave!(aug_handler, true) if supported?(:post_resource_eval)
  end

  # Returns a node label to use for creating a new entry in an Augeas sequence
  # (seq), given the current list, e.g. '1' if it's the first.  Supply
  # aug.match('$target/*') or similar.
  #
  # @param [Array<String>] existing paths, from Augeas#match
  # @return [String] new node label
  def next_seq(matches)
    self.class.next_seq(matches)
  end
end
