lib/utils/config_file.rb



require 'tins'

# Configuration file manager for Utils library.
#
# This class provides functionality for loading, parsing, and managing
# configuration settings from multiple sources. It supports DSL-style
# configuration blocks and integrates with various utility components to
# provide centralized configuration management.
class Utils::ConfigFile
  class << self

    # The config_file_paths accessor method provides read and write access to
    # the config_file_paths instance variable.
    #
    # @return [ Array<String> ] the array of configuration file paths
    attr_accessor :config_file_paths
  end
  self.config_file_paths = [
    '/etc/utilsrc',
    '~/.utilsrc',
    './.utilsrc',
  ]

  include DSLKit::Interpreter

  # Error raised when configuration file parsing fails.
  #
  # This exception is specifically designed to be thrown when issues occur
  # during the parsing or processing of configuration files within the Utils
  # library. It inherits from StandardError, making it a standard Ruby
  # exception that can be caught and handled appropriately by calling code.
  class ConfigFileError < StandardError; end

  # The initialize method sets up a new instance of the class.
  #
  # This method is called when creating a new object and performs any necessary
  # initialization tasks for the instance variables and internal state.
  def initialize
  end

  # The configure_from_paths method initializes the configuration by parsing
  # configuration files from the specified paths.
  #
  # This method iterates through an array of configuration file paths and
  # processes each one to load the configuration settings. It is typically used
  # to set up the application's configuration from multiple sources.
  #
  # @param paths [ Array<String> ] an array of file paths pointing to configuration files
  def configure_from_paths(paths = self.class.config_file_paths)
    for config_file_path in paths
      parse_config_file config_file_path
    end
  end

  # The parse_config_file method reads and processes a configuration file.
  #
  # This method opens the specified configuration file, reads its contents,
  # and parses the configuration data. It handles file path expansion and
  # includes error handling for system call errors during file operations.
  #
  # @param config_file_path [ String ] the path to the configuration file to be parsed
  #
  # @return [ Utils::ConfigFile ] returns self after parsing the configuration file
  #
  # @raise [ SystemCallError ] if there is an issue reading the configuration file
  #
  # @note The method will output a warning message to standard error if it fails
  #       to read the configuration file and return nil.
  def parse_config_file(config_file_path)
    config_file_path = File.expand_path(config_file_path)
    File.open(config_file_path) do |cf|
      parse cf.read
    end
    self
  rescue SystemCallError => e
    $DEBUG and warn "Couldn't read config file "\
      "#{config_file_path.inspect}: #{e.class} #{e}"
    return nil
  end

  # The parse method processes the provided source code by interpreting it
  # within the given binding context.
  #
  # This method takes a source code string and evaluates it in the context of
  # the specified binding, allowing for dynamic execution of code with access
  # to the current variable scope.
  #
  # @param source [ String ] the source code to be interpreted and executed
  #
  # @return [ Object ] returns self after processing the source code
  def parse(source)
    interpret_with_binding source, binding
    self
  end

  # Base class for defining configuration blocks with DSL accessors.
  #
  # This class provides a foundation for creating configuration classes that
  # support dynamic attribute definition through DSL-style accessor methods. It
  # includes functionality for registering configuration settings and
  # generating Ruby code representations of the configuration state.
  class BlockConfig
    class << self
      # The inherited method extends the module with DSL accessor functionality
      # and calls the superclass implementation.
      #
      # @param modul [ Module ] the module that inherited this class
      def inherited(modul)
        modul.extend DSLKit::DSLAccessor
        super
      end

      # The config method sets up a configuration accessor with the specified
      # name and options.
      #
      # This method registers a new configuration setting by adding it to the
      # list of configuration settings and then creates an accessor for it
      # using the dsl_accessor method, allowing for easy retrieval and
      # assignment of configuration values.
      #
      # @param name [ Object ] the name of the configuration setting
      # @param r [ Array ] additional arguments passed to the dsl_accessor method
      #
      # @yield [ block ] optional block to be passed to the dsl_accessor method
      #
      # @return [ Object ] returns self to allow for method chaining
      def config(name, *r, &block)
        self.config_settings ||= []
        config_settings << name.to_sym
        dsl_accessor name, *r, &block
        self
      end

      # The lazy_config method configures a lazy-loaded configuration option
      # with a default value.
      #
      # This method registers a new configuration setting that will be
      # initialized lazily, meaning the default value or the set value is only
      # computed when the configuration is actually accessed. It adds the
      # setting to the list of configuration settings and creates a lazy
      # accessor for it.
      #
      # @param name [ Object ] the name of the configuration setting to define
      # @yield [ default ] optional block that provides the default value for
      # the configuration
      #
      # @return [ Object ] returns self to allow for method chaining
      def lazy_config(name, &default)
        self.config_settings ||= []
        config_settings << name.to_sym
        dsl_lazy_accessor(name, &default)
        self
      end

      # The config_settings method provides access to the configuration
      # settings.
      #
      # This method returns the configuration settings stored in the instance
      # variable, allowing for reading and modification of the object's
      # configuration state.
      #
      # @return [ Object ] the current configuration settings stored in the
      # instance variable
      attr_accessor :config_settings
    end

    # The initialize method sets up the instance by evaluating the provided
    # block in the instance's context.
    #
    # This method allows for dynamic configuration of the object by executing
    # the given block within the instance's scope, enabling flexible
    # initialization patterns.
    #
    # @param block [ Proc ] the block to be evaluated for instance setup
    def initialize(&block)
      block and instance_eval(&block)
    end

    # The to_ruby method generates a Ruby configuration block representation by
    # recursively processing the object's configuration settings and their
    # values.
    # It creates a nested structure with proper indentation and formatting
    # suitable for configuration files.
    #
    # @param depth [ Integer ] the current nesting depth for indentation purposes
    #
    # @return [ String ] a formatted Ruby string representing the configuration block
    def to_ruby(depth = 0)
      result = ''
      result << ' ' * 2 * depth <<
        "#{self.class.name[/::([^:]+)\z/, 1].underscore} do\n"
      for name in self.class.config_settings
        value = __send__(name)
        if value.respond_to?(:to_ruby)
          result << ' ' * 2 * (depth + 1) << value.to_ruby(depth + 1)
        else
          result << ' ' * 2 * (depth + 1) <<
            "#{name} #{Array(value).map(&:inspect) * ', '}\n"
        end
      end
      result << ' ' * 2 * depth << "end\n"
    end
  end

  # A configuration class for test execution settings.
  #
  # This class manages the configuration options related to running tests,
  # specifically supporting different test frameworks and defining which
  # directories should be included in test discovery and execution.
  class Probe < BlockConfig
    # The config method sets up a configuration option for the test framework.
    #
    # This method defines a configuration parameter that specifies which test
    # framework should be used, allowing for flexible test execution
    # environments.
    #
    # @param name [ Symbol ] the name of the configuration option
    # @param value [ Object ] the value to set for the configuration option
    config :test_framework, :'test-unit'

    # The include_dirs method configures the directories to be included in the
    # search.
    #
    # @param dirs [ Array<String> ] the array of directory names to include
    config :include_dirs, %w[lib test tests ext spec]

    # The include_dirs_argument method constructs a colon-separated string from
    # include directories.
    #
    # This method takes the include directories configuration and converts it
    # into a single colon-delimited string suitable for use in command-line
    # arguments or environment variables.
    #
    # @return [ String ] a colon-separated string of include directory paths
    def include_dirs_argument
      Array(include_dirs) * ':'
    end

    # The initialize method sets up the configuration by validating the test
    # framework.
    #
    # This method initializes the configuration object and ensures that the
    # specified test framework is one of the allowed values. It raises an error
    # if the test framework is not supported.
    #
    # @param block [ Proc ] a block to be passed to the superclass initializer
    def initialize(&block)
      super
      test_frameworks_allowed = [ :'test-unit', :rspec ]
      test_frameworks_allowed.include?(test_framework) or
        raise ConfigFileError,
          "test_framework has to be in #{test_frameworks_allowed.inspect}"
    end
  end

  # The probe method initializes and returns a Probe object.
  #
  # This method creates a new Probe instance either from the provided block or
  # with default settings, storing it for later use. It ensures that only one
  # Probe instance is created per object, returning the existing instance
  # on subsequent calls.
  #
  # @param block [ Proc ] optional block to configure the Probe instance
  #
  # @return [ Utils::Probe ] a Probe instance configured either by the block
  #         or with default settings
  def probe(&block)
    if block
      @probe = Probe.new(&block)
    end
    @probe ||= Probe.new
  end

  # A configuration class for file system operations.
  #
  # This class manages the configuration settings for searching and discovering
  # files and directories while filtering out unwanted entries based on
  # configured patterns. It provides functionality to define which directories
  # should be pruned and which files should be skipped during file system
  # operations.
  class FileFinder < BlockConfig
    # The prune? method checks if a basename matches any of the configured
    # prune directories.
    #
    # This method determines whether a given filename or directory name should
    # be excluded based on the prune directories configuration. It iterates
    # through the list of prune patterns and returns true if any pattern
    # matches the provided basename.
    #
    # @param basename [ String, Object ] the basename to check against prune patterns
    #
    # @return [ TrueClass, FalseClass ] true if the basename matches any prune pattern,
    #         false otherwise
    def prune?(basename)
      Array(prune_dirs).any? { |pd| pd.match(basename.to_s) }
    end

    # The skip? method determines whether a file should be skipped based on
    # configured patterns.
    #
    # This method checks if the provided basename matches any of the configured
    # skip patterns. It converts the basename to a string and tests it against
    # all defined skip files.
    #
    # @param basename [ Object] the file or directory name to check
    #
    # @return [ TrueClass, FalseClass ] true if the basename matches any skip
    # pattern, false otherwise
    def skip?(basename)
      Array(skip_files).any? { |sf| sf.match(basename.to_s) }
    end
  end

  # A configuration class for search operations.
  #
  # This class manages the configuration settings for searching files and
  # directories while filtering out unwanted entries based on configured
  # patterns. It inherits from FileFinder and provides functionality to define
  # which directories should be pruned and which files should be skipped during
  # search processes.
  class Search < FileFinder
    # The prune_dirs method configures the pattern for identifying directories
    # to be pruned during file system operations.
    #
    # This method sets up a regular expression pattern that matches directory
    # names which should be excluded from processing. The default pattern
    # excludes version control directories (.svn, .git, CVS) and temporary
    # directories (tmp).
    #
    # @param first [ Regexp ] the regular expression pattern for matching
    # directories to prune
    config :prune_dirs, /\A(\.svn|\.git|CVS|tmp)\z/

    # The skip_files configuration method sets up a regular expression pattern
    # for filtering out files based on their names.
    #
    # This method configures a pattern that matches filenames which should be
    # skipped during file processing operations.
    # It uses a regular expression to identify files that start with a dot, end
    # with common temporary file extensions, or match other patterns typically
    # associated with backup, swap, log, or temporary files.
    #
    # @param pattern [ Regexp ] the regular expression pattern used to identify
    # files to skip
    config :skip_files, /(\A\.|\.sw[pon]\z|\.log\z|~\z)/
  end

  # The search method initializes and returns a Search object.
  #
  # This method creates a Search instance either from a provided block or with
  # default settings.
  # It maintains a cached instance of the Search object, returning the same
  # instance on subsequent calls.
  #
  # @param block [ Proc ] optional block to configure the Search object
  #
  # @return [ Utils::Search ] a Search object configured either by the provided
  # block or with default settings
  def search(&block)
    if block
      @search = Search.new(&block)
    end
    @search ||= Search.new
  end

  # A configuration class for file discovery operations.
  #
  # This class manages the configuration settings for discovering files and directories
  # while filtering out unwanted entries based on configured patterns. It inherits from
  # FileFinder and provides functionality to define which directories should be pruned
  # and which files should be skipped during discovery processes.
  class Discover < FileFinder
    # The prune_dirs method configures the pattern for identifying directories
    # to be pruned during file system operations.
    #
    # This method sets up a regular expression pattern that matches directory
    # names which should be excluded from processing.
    # The default pattern excludes version control directories (.svn, .git,
    # CVS) and temporary directories (tmp).
    #
    # @param first [ Regexp ] the regular expression pattern for matching
    # directories to prune
    config :prune_dirs, /\A(\.svn|\.git|CVS|tmp)\z/

    # The skip_files configuration method sets up a regular expression pattern
    # for filtering out files based on their names.
    #
    # This method configures a pattern that matches filenames which should be
    # skipped during file processing operations. It uses a regular expression
    # to identify files that start with a dot, end with common temporary file
    # extensions, or match other patterns typically associated with backup,
    # swap, log, or temporary files.
    #
    # @param pattern [ Regexp ] the regular expression pattern used to identify
    # files to skip
    config :skip_files, /(\A\.|\.sw[pon]\z|\.log\z|~\z)/

    # The config method sets up a configuration option with a default value.
    #
    # @param name [ Symbol ] the name of the configuration option
    # @param default [ Object ] the default value for the configuration option
    config :max_matches, 10

    # The index_expire_after method configures the expiration time for index
    # files.
    #
    # This method sets up the duration after which index files should be
    # considered expired and potentially refreshed or regenerated by the
    # system.
    #
    # @param value [ Integer, nil ] the number of seconds after which indexes expire,
    #                               or nil to disable automatic expiration
    config :index_expire_after
  end

  # The discover method initializes and returns a Discover object.
  #
  # This method sets up a Discover instance, either using a provided block for
  # configuration or creating a default instance. It ensures that only one
  # Discover object is created per instance by storing it in an instance
  # variable.
  #
  # @param block [ Proc ] optional block to configure the Discover object
  #
  # @return [ Utils::Discover ] a Discover object configured either with the
  # provided block or with default settings
  def discover(&block)
    if block
      @discover = Discover.new(&block)
    end
    @discover ||= Discover.new
  end

  # A configuration class for code indexing operations.
  #
  # This class manages the configuration settings for generating code indexes
  # like ctags and cscope. It provides functionality to define which paths
  # should be indexed and what file formats should be generated for each
  # indexing tool.
  #
  # @example
  #   indexer = Utils::ConfigFile.new.code_indexer do |config|
  #     config.paths = %w[ lib spec ]
  #     config.formats = { 'ctags' => 'tags', 'cscope' => 'cscope.out' }
  #   end
  #
  # The paths config configures the directories to be included in the index
  # generation process.
  #
  # The formats config configures the output file formats for different indexing
  # tools and the output filenames.
  class CodeIndexer < BlockConfig
    config :verbose, false

    lazy_config :paths do
      %w[ bin lib spec tests ]
    end

    config :formats, {
      'ctags'  => 'tags',
      'cscope' => 'cscope.out',
    }
  end

  # The code_indexer method manages and returns a CodeIndexer configuration
  # instance.
  #
  # This method provides access to a CodeIndexer object that handles configuration
  # for generating code indexes such as ctags and cscope files. It ensures that only
  # one CodeIndexer instance is created per object by storing it in an instance variable.
  # When a block is provided, it initializes the CodeIndexer with custom settings;
  # otherwise, it returns a default CodeIndexer instance.
  #
  # @param block [ Proc ] optional block to configure the CodeIndexer object
  #
  # @return [ Utils::ConfigFile::CodeIndexer ] a CodeIndexer configuration instance
  #         configured either by the block or with default settings
  def code_indexer(&block)
    if block
      @code_indexer = CodeIndexer.new(&block)
    end
    @code_indexer ||= CodeIndexer.new
  end

  # A configuration class for whitespace handling operations.
  #
  # This class manages the configuration options related to removing or modifying
  # trailing whitespace in files. It provides functionality to define patterns for
  # pruning directories and skipping specific files during whitespace processing,
  # ensuring that only relevant files are affected by space-stripping operations.
  class StripSpaces < FileFinder
    # The prune_dirs method configures the pattern for directory names that
    # should be pruned.
    #
    # This method sets up a regular expression pattern that identifies
    # directories which should be excluded or removed during file system
    # operations.
    #
    # @param pattern [ Regexp ] the regular expression pattern to match
    # directory names
    config :prune_dirs, /\A(\..*|CVS)\z/

    # The skip_files configuration method sets up a regular expression pattern
    # for filtering out files based on their names.
    #
    # This method configures a pattern that matches filenames which should be
    # skipped during file processing operations.
    # It uses a regular expression to identify files that start with a dot, end
    # with common temporary file extensions, or match other patterns typically
    # associated with backup, swap, log, or temporary files.
    #
    # @param pattern [ Regexp ] the regular expression pattern used to identify
    # files to skip
    config :skip_files, /(\A\.|\.sw[pon]\z|\.log\z|~\z)/
  end

  # The strip_spaces method configures and returns a StripSpaces object for
  # processing whitespace.
  #
  # This method initializes a StripSpaces processor that can be used to remove
  # or modify whitespace in strings. When a block is provided, it sets up the
  # processor with custom behavior defined by the block. Otherwise, it returns
  # a default StripSpaces instance.
  #
  # @param block [ Proc ] optional block to customize the strip spaces behavior
  #
  # @return [ Utils::StripSpaces ] a configured StripSpaces processor instance
  def strip_spaces(&block)
    if block
      @strip_spaces = StripSpaces.new(&block)
    end
    @strip_spaces ||= StripSpaces.new
  end

  # SSH tunnel configuration manager
  #
  # Provides functionality for configuring and managing SSH tunnels with support for
  # different terminal multiplexers like tmux and screen. Allows setting up tunnel
  # specifications with local and remote address/port combinations, handling
  # environment variables, and managing copy/paste functionality for tunnel sessions.
  class SshTunnel < BlockConfig
    # The terminal_multiplexer method configures the terminal multiplexer
    # setting.
    #
    # This method sets up the terminal multiplexer that will be used for
    # managing multiple terminal sessions within the application environment.
    #
    # @param value [ String ] the name of the terminal multiplexer to use
    config :terminal_multiplexer, 'tmux'

    # The env method configures the environment variables for the session.
    #
    # @param value [ Hash ] environment variable hash
    config :env, {}

    # The login_session method configures the login session settings.
    #
    # @param block [ Proc ] a block containing the login session configuration
    config :login_session do
      ENV.fetch('HOME',  'session')
    end

    # The initialize method sets up the instance by calling the superclass
    # constructor and assigning the terminal multiplexer configuration.
    def initialize
      super
      self.terminal_multiplexer = terminal_multiplexer
    end

    # The terminal_multiplexer= method sets the terminal multiplexer type for
    # the editor.
    #
    # This method assigns the specified terminal multiplexer to the editor
    # configuration, validating that it is either 'screen' or 'tmux'. It
    # converts the input to a string and ensures it matches one of the
    # supported multiplexer types.
    #
    # @param terminal_multiplexer [ Symbol ] the terminal multiplexer type to
    # be configured
    def terminal_multiplexer=(terminal_multiplexer)
      @multiplexer = terminal_multiplexer.to_s
      @multiplexer =~ /\A(screen|tmux)\z/ or
        fail "invalid terminal_multiplexer #{terminal_multiplexer.inspect} was configured"
    end

    # The multiplexer_list method returns the appropriate command string for
    # listing sessions based on the current multiplexer type.
    #
    # @return [ String, nil ] the command string to list sessions for the configured
    #         multiplexer ('screen -ls' or 'tmux ls'), or nil if no multiplexer is set
    def multiplexer_list
      case @multiplexer
      when 'screen'
        'screen -ls'
      when 'tmux'
        'tmux ls'
      end
    end

    # The multiplexer_new method generates a command string for creating a new
    # session in the specified terminal multiplexer.
    #
    # @param session [ String ] the name of the session to be created
    #
    # @return [ String, nil ] a command string for creating a new session in screen
    #         or tmux, or nil if the multiplexer type is not supported
    def multiplexer_new(session)
      case @multiplexer
      when 'screen'
        'false'
      when 'tmux'
        'tmux -u new -s "%s"' % session
      end
    end

    # The multiplexer_attach method generates a command string for attaching to
    # a session using either screen or tmux multiplexer.
    #
    # @param session [ String ] the name or identifier of the session to attach to
    #
    # @return [ String ] a formatted command string ready for execution
    def multiplexer_attach(session)
      case @multiplexer
      when 'screen'
        'screen -DUR "%s"' % session
      when 'tmux'
        'tmux -u attach -d -t "%s"' % session
      end
    end

    # Manages the copy/paste functionality configuration for SSH tunnels.
    #
    # This class handles the setup and configuration of copy/paste capabilities
    # within SSH tunnel sessions, allowing users to define network addresses,
    # ports, and other parameters needed for establishing and managing
    # copy/paste connections through SSH tunnels.
    class CopyPaste < BlockConfig
      # The bind_address method configures the network address to which the
      # server will bind for incoming connections.
      #
      # @param value [ String ] the IP address or hostname to bind the server to
      config :bind_address, 'localhost'

      # The port method configures the port number for the SSH tunnel
      # specification.
      #
      # This method sets up the port component of an SSH tunnel configuration,
      # allowing for the specification of a network port to be used in the
      # tunnel.
      #
      # @param name [ String ] the name of the port configuration
      # @param default [ Integer ] the default port number to use if none is specified
      config :port, 6166

      # The host method configures the hostname for the SSH tunnel specification.
      #
      # This method sets up the host parameter that will be used in the SSH tunnel
      # configuration, allowing connections to be established through the specified
      # host address.
      #
      # @param value [ String ] the hostname or IP address to use for the SSH tunnel
      config :host, 'localhost'

      # The host_port method configures the host port setting for the
      # application.
      #
      # @param value [ Integer ] the port number to be used for host communication
      config :host_port, 6166

      # The to_s method returns a colon-separated string representation of the
      # SSH tunnel specification.
      #
      # This method combines the bind address, port, host, and host port components
      # into a single string format using colons as separators.
      #
      # @return [ String ] a colon-separated string containing the tunnel specification
      #         in the format "bind_address:port:host:host_port"
      def to_s
        [ bind_address, port, host, host_port ] * ':'
      end
    end

    # The copy_paste method manages the copy-paste functionality by returning
    # an existing instance or creating a new one.
    #
    # This method checks if a copy-paste instance already exists and returns it
    # if available. If no instance exists, it creates a new one based on
    # whether a block is provided or
    # the enable flag is set to true.
    #
    # @param enable [ TrueClass, FalseClass ] flag to enable copy-paste functionality
    #
    # @yield [ block ] optional block to initialize the copy-paste instance
    #
    # @return [ CopyPaste, nil ] the existing or newly created copy-paste
    # instance, or nil if not enabled
    def copy_paste(enable = false, &block)
      if @copy_paste
        @copy_paste
      else
        if block
          @copy_paste = CopyPaste.new(&block)
        elsif enable
          @copy_paste = CopyPaste.new {}
        end
      end
    end
    self.config_settings << :copy_paste
  end

  # The ssh_tunnel method provides access to an SSH tunnel configuration instance.
  #
  # This method returns the existing SSH tunnel configuration object if one has
  # already been created, or initializes and returns a new SSH tunnel
  # configuration instance if no instance exists. If a block is provided, it
  # will be passed to the SSH tunnel configuration constructor when creating a
  # new instance.
  #
  # @param block [ Proc ] optional block to configure the SSH tunnel object
  #
  # @return [ Utils::ConfigFile::SshTunnel ] an SSH tunnel configuration instance
  def ssh_tunnel(&block)
    if block
      @ssh_tunnel = SshTunnel.new(&block)
    end
    @ssh_tunnel ||= SshTunnel.new
  end

  # A configuration class for editor settings.
  #
  # This class manages the configuration options related to editing operations,
  # specifically focusing on Vim editor integration. It provides functionality
  # to configure the path to the Vim executable and default arguments used when
  # invoking the editor.
  class Edit < BlockConfig
    # The vim_path method determines the path to the vim executable.
    #
    # This method executes the which command to locate the vim executable in
    # the system's PATH and returns the resulting path after stripping any
    # trailing whitespace.
    #
    # @return [ String ] the full path to the vim executable as determined by the which command
    config :vim_path do `which vim`.chomp end

    config :vim_default_args, nil
  end

  # The edit method initializes and returns an Edit object.
  #
  # This method creates an Edit instance either from a provided block or with
  # default settings. It stores the Edit object as an instance variable and
  # returns it on subsequent calls.
  #
  # @param block [ Proc ] optional block to configure the Edit object
  #
  # @return [ Edit ] an Edit object configured either by the block or with default settings
  def edit(&block)
    if block
      @edit = Edit.new(&block)
    end
    @edit ||= Edit.new
  end

  # A configuration class for file classification settings.
  #
  # This class manages the configuration options related to classifying files
  # by type or category. It provides functionality to define path shifting
  # behavior and prefix handling for determining how file paths should be
  # categorized during classification operations.
  class Classify < BlockConfig
    # The shift_path_by_default method configuration accessor
    #
    # This method provides access to the shift_path_by_default configuration
    # setting which determines the default path shifting value used in various
    # operations.
    #
    # @return [ Integer ] the default path shifting value configured for the system
    config :shift_path_by_default, 0

    # The shift_path_for_prefix method configures path shifting behavior for
    # prefix handling.
    #
    # This method sets up the configuration for how paths should be shifted
    # when dealing with prefix-based operations, typically used in file system
    # or directory navigation contexts.
    #
    # @param config [ Array ] the configuration array for shift path settings
    config :shift_path_for_prefix, []
  end

  # The classify method initializes and returns a Classify object.
  #
  # This method creates a Classify instance either from the provided block or
  # with default settings if no block is given. It ensures that only one
  # Classify object is created per instance by storing it in an instance variable.
  #
  # @param block [ Proc ] optional block to configure the Classify object
  #
  # @return [ Classify ] a Classify object configured either by the block or with defaults
  def classify(&block)
    if block
      @classify = Classify.new(&block)
    end
    @classify ||= Classify.new
  end

  # A configuration class for directory synchronization settings.
  #
  # This class manages the configuration options related to synchronizing
  # directories using rsync. It provides functionality to define patterns for
  # skipping certain paths during synchronization operations, making it easy to
  # exclude temporary, cache, or version control files from being synced.
  class SyncDir < BlockConfig
    # The skip_path method configures a regular expression pattern for skipping
    # paths.
    #
    # This method sets up a configuration option that defines a regular
    # expression used to identify and skip certain paths during processing
    # operations.
    #
    # @param pattern [ Regexp ] the regular expression pattern used to match
    # paths to be skipped
    config :skip_path, %r((\A|/)\.\w)

    # The skip? method determines whether a given path should be skipped based
    # on the skip_path pattern.
    #
    # This method checks if the provided path matches the internal skip_path
    # regular expression, returning true if the path should be excluded from
    # processing, or false otherwise.
    #
    # @param path [ String ] the path to check against the skip pattern
    #
    # @return [ TrueClass, FalseClass ] true if the path matches the skip
    # pattern, false otherwise
    def skip?(path)
      path =~ skip_path
    end
  end

  # The sync_dir method provides access to a SyncDir instance.
  #
  # This method returns the existing SyncDir instance if one has already been
  # created, or initializes and returns a new SyncDir instance if no instance
  # exists. If a block is provided, it will be passed to the SyncDir constructor
  # when creating a new instance.
  #
  # @return [ SyncDir ] the SyncDir instance associated with this object
  def sync_dir(&block)
    if block
      @sync_dir = SyncDir.new(&block)
    end
    @sync_dir ||= SyncDir.new
  end

  # A configuration class for code comment settings.
  #
  # This class manages the configuration options related to generating YARD
  # documentation for Ruby source code. It provides access to glob patterns
  # that define which files should be considered when generating code comments.
  class CodeComment < BlockConfig
    dsl_accessor :code_globs, 'lib/**/*.rb', 'spec/**/*.rb', 'tests/**/*rb'
  end

  # The code_comment method provides access to a CodeComment configuration
  # instance.
  #
  # This method returns the existing CodeComment instance if one has already
  # been created, or initializes and returns a new CodeComment instance if no
  # instance exists. If a block is provided, it will be passed to the
  # CodeComment constructor when creating a new instance.
  #
  # @param block [ Proc ] optional block to configure the CodeComment object
  #
  # @return [ Utils::ConfigFile::CodeComment ] a CodeComment configuration instance
  def code_comment(&block)
    if block
      @code_comment = CodeComment.new(&block)
    end
    @code_comment ||= CodeComment.new
  end

  # The to_ruby method generates a Ruby configuration string by collecting
  # configuration data from various components and combining them into a
  # single formatted output.
  #
  # @return [ String ] a Ruby formatted string containing configuration
  #         settings from search, discover, strip_spaces, probe, ssh_tunnel,
  #         edit, and classify components
  def to_ruby
    result = "# vim: set ft=ruby:\n"
    for bc in %w[search discover strip_spaces probe ssh_tunnel edit classify]
      result << "\n" << __send__(bc).to_ruby
    end
    result
  end
end